Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2006, 2007 Google Inc. All Rights Reserved.
      4 # Author: danderson (at] google.com (David Anderson)
      5 #
      6 # Script for uploading files to a Google Code project.
      7 #
      8 # This is intended to be both a useful script for people who want to
      9 # streamline project uploads and a reference implementation for
     10 # uploading files to Google Code projects.
     11 #
     12 # To upload a file to Google Code, you need to provide a path to the
     13 # file on your local machine, a small summary of what the file is, a
     14 # project name, and a valid account that is a member or owner of that
     15 # project.  You can optionally provide a list of labels that apply to
     16 # the file.  The file will be uploaded under the same name that it has
     17 # in your local filesystem (that is, the "basename" or last path
     18 # component).  Run the script with '--help' to get the exact syntax
     19 # and available options.
     20 #
     21 # Note that the upload script requests that you enter your
     22 # googlecode.com password.  This is NOT your Gmail account password!
     23 # This is the password you use on googlecode.com for committing to
     24 # Subversion and uploading files.  You can find your password by going
     25 # to http://code.google.com/hosting/settings when logged in with your
     26 # Gmail account. If you have already committed to your project's
     27 # Subversion repository, the script will automatically retrieve your
     28 # credentials from there (unless disabled, see the output of '--help'
     29 # for details).
     30 #
     31 # If you are looking at this script as a reference for implementing
     32 # your own Google Code file uploader, then you should take a look at
     33 # the upload() function, which is the meat of the uploader.  You
     34 # basically need to build a multipart/form-data POST request with the
     35 # right fields and send it to https://PROJECT.googlecode.com/files .
     36 # Authenticate the request using HTTP Basic authentication, as is
     37 # shown below.
     38 #
     39 # Licensed under the terms of the Apache Software License 2.0:
     40 #  http://www.apache.org/licenses/LICENSE-2.0
     41 #
     42 # Questions, comments, feature requests and patches are most welcome.
     43 # Please direct all of these to the Google Code users group:
     44 #  http://groups.google.com/group/google-code-hosting
     45 
     46 """Google Code file uploader script.
     47 """
     48 
     49 __author__ = 'danderson (at] google.com (David Anderson)'
     50 
     51 import httplib
     52 import os.path
     53 import optparse
     54 import getpass
     55 import base64
     56 import sys
     57 
     58 
     59 def upload(file, project_name, user_name, password, summary, labels=None):
     60   """Upload a file to a Google Code project's file server.
     61 
     62   Args:
     63     file: The local path to the file.
     64     project_name: The name of your project on Google Code.
     65     user_name: Your Google account name.
     66     password: The googlecode.com password for your account.
     67               Note that this is NOT your global Google Account password!
     68     summary: A small description for the file.
     69     labels: an optional list of label strings with which to tag the file.
     70 
     71   Returns: a tuple:
     72     http_status: 201 if the upload succeeded, something else if an
     73                  error occured.
     74     http_reason: The human-readable string associated with http_status
     75     file_url: If the upload succeeded, the URL of the file on Google
     76               Code, None otherwise.
     77   """
     78   # The login is the user part of user (at] gmail.com. If the login provided
     79   # is in the full user@domain form, strip it down.
     80   if user_name.endswith('@gmail.com'):
     81     user_name = user_name[:user_name.index('@gmail.com')]
     82 
     83   form_fields = [('summary', summary)]
     84   if labels is not None:
     85     form_fields.extend([('label', l.strip()) for l in labels])
     86 
     87   content_type, body = encode_upload_request(form_fields, file)
     88 
     89   upload_host = '%s.googlecode.com' % project_name
     90   upload_uri = '/files'
     91   auth_token = base64.b64encode('%s:%s'% (user_name, password))
     92   headers = {
     93     'Authorization': 'Basic %s' % auth_token,
     94     'User-Agent': 'Googlecode.com uploader v0.9.4',
     95     'Content-Type': content_type,
     96     }
     97 
     98   server = httplib.HTTPSConnection(upload_host)
     99   server.request('POST', upload_uri, body, headers)
    100   resp = server.getresponse()
    101   server.close()
    102 
    103   if resp.status == 201:
    104     location = resp.getheader('Location', None)
    105   else:
    106     location = None
    107   return resp.status, resp.reason, location
    108 
    109 
    110 def encode_upload_request(fields, file_path):
    111   """Encode the given fields and file into a multipart form body.
    112 
    113   fields is a sequence of (name, value) pairs. file is the path of
    114   the file to upload. The file will be uploaded to Google Code with
    115   the same file name.
    116 
    117   Returns: (content_type, body) ready for httplib.HTTP instance
    118   """
    119   BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
    120   CRLF = '\r\n'
    121 
    122   body = []
    123 
    124   # Add the metadata about the upload first
    125   for key, value in fields:
    126     body.extend(
    127       ['--' + BOUNDARY,
    128        'Content-Disposition: form-data; name="%s"' % key,
    129        '',
    130        value,
    131        ])
    132 
    133   # Now add the file itself
    134   file_name = os.path.basename(file_path)
    135   f = open(file_path, 'rb')
    136   file_content = f.read()
    137   f.close()
    138 
    139   body.extend(
    140     ['--' + BOUNDARY,
    141      'Content-Disposition: form-data; name="filename"; filename="%s"'
    142      % file_name,
    143      # The upload server determines the mime-type, no need to set it.
    144      'Content-Type: application/octet-stream',
    145      '',
    146      file_content,
    147      ])
    148 
    149   # Finalize the form body
    150   body.extend(['--' + BOUNDARY + '--', ''])
    151 
    152   return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
    153 
    154 
    155 def upload_find_auth(file_path, project_name, summary, labels=None,
    156                      user_name=None, password=None, tries=3):
    157   """Find credentials and upload a file to a Google Code project's file server.
    158 
    159   file_path, project_name, summary, and labels are passed as-is to upload.
    160 
    161   Args:
    162     file_path: The local path to the file.
    163     project_name: The name of your project on Google Code.
    164     summary: A small description for the file.
    165     labels: an optional list of label strings with which to tag the file.
    166     config_dir: Path to Subversion configuration directory, 'none', or None.
    167     user_name: Your Google account name.
    168     tries: How many attempts to make.
    169   """
    170   if user_name is None or password is None:
    171     from netrc import netrc
    172     authenticators = netrc().authenticators("code.google.com")
    173     if authenticators:
    174       if user_name is None:
    175         user_name = authenticators[0]
    176       if password is None:
    177         password = authenticators[2]
    178 
    179   while tries > 0:
    180     if user_name is None:
    181       # Read username if not specified or loaded from svn config, or on
    182       # subsequent tries.
    183       sys.stdout.write('Please enter your googlecode.com username: ')
    184       sys.stdout.flush()
    185       user_name = sys.stdin.readline().rstrip()
    186     if password is None:
    187       # Read password if not loaded from svn config, or on subsequent tries.
    188       print 'Please enter your googlecode.com password.'
    189       print '** Note that this is NOT your Gmail account password! **'
    190       print 'It is the password you use to access Subversion repositories,'
    191       print 'and can be found here: http://code.google.com/hosting/settings'
    192       password = getpass.getpass()
    193 
    194     status, reason, url = upload(file_path, project_name, user_name, password,
    195                                  summary, labels)
    196     # Returns 403 Forbidden instead of 401 Unauthorized for bad
    197     # credentials as of 2007-07-17.
    198     if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
    199       # Rest for another try.
    200       user_name = password = None
    201       tries = tries - 1
    202     else:
    203       # We're done.
    204       break
    205 
    206   return status, reason, url
    207 
    208 
    209 def main():
    210   parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
    211                                  '-p PROJECT [options] FILE')
    212   parser.add_option('-s', '--summary', dest='summary',
    213                     help='Short description of the file')
    214   parser.add_option('-p', '--project', dest='project',
    215                     help='Google Code project name')
    216   parser.add_option('-u', '--user', dest='user',
    217                     help='Your Google Code username')
    218   parser.add_option('-w', '--password', dest='password',
    219                     help='Your Google Code password')
    220   parser.add_option('-l', '--labels', dest='labels',
    221                     help='An optional list of comma-separated labels to attach '
    222                     'to the file')
    223 
    224   options, args = parser.parse_args()
    225 
    226   if not options.summary:
    227     parser.error('File summary is missing.')
    228   elif not options.project:
    229     parser.error('Project name is missing.')
    230   elif len(args) < 1:
    231     parser.error('File to upload not provided.')
    232   elif len(args) > 1:
    233     parser.error('Only one file may be specified.')
    234 
    235   file_path = args[0]
    236 
    237   if options.labels:
    238     labels = options.labels.split(',')
    239   else:
    240     labels = None
    241 
    242   status, reason, url = upload_find_auth(file_path, options.project,
    243                                          options.summary, labels,
    244                                          options.user, options.password)
    245   if url:
    246     print 'The file was uploaded successfully.'
    247     print 'URL: %s' % url
    248     return 0
    249   else:
    250     print 'An error occurred. Your file was not uploaded.'
    251     print 'Google Code upload server said: %s (%s)' % (reason, status)
    252     return 1
    253 
    254 
    255 if __name__ == '__main__':
    256   sys.exit(main())
    257