1#!/usr/bin/env python
2#
3# Copyright 2006, 2007 Google Inc. All Rights Reserved.
4# Author: danderson@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# Licensed under the Apache License, Version 2.0 (the "License");
47# you may not use this file except in compliance with the License.
48# You may obtain a copy of the License at
49#
50#      http://www.apache.org/licenses/LICENSE-2.0
51
52"""Google Code file uploader script.
53"""
54
55__author__ = 'danderson@google.com (David Anderson)'
56
57import httplib
58import os.path
59import optparse
60import getpass
61import base64
62import sys
63
64
65def upload(file, project_name, user_name, password, summary, labels=None):
66  """Upload a file to a Google Code project's file server.
67
68  Args:
69    file: The local path to the file.
70    project_name: The name of your project on Google Code.
71    user_name: Your Google account name.
72    password: The googlecode.com password for your account.
73              Note that this is NOT your global Google Account password!
74    summary: A small description for the file.
75    labels: an optional list of label strings with which to tag the file.
76
77  Returns: a tuple:
78    http_status: 201 if the upload succeeded, something else if an
79                 error occured.
80    http_reason: The human-readable string associated with http_status
81    file_url: If the upload succeeded, the URL of the file on Google
82              Code, None otherwise.
83  """
84  # The login is the user part of user@gmail.com. If the login provided
85  # is in the full user@domain form, strip it down.
86  if user_name.endswith('@gmail.com'):
87    user_name = user_name[:user_name.index('@gmail.com')]
88
89  form_fields = [('summary', summary)]
90  if labels is not None:
91    form_fields.extend([('label', l.strip()) for l in labels])
92
93  content_type, body = encode_upload_request(form_fields, file)
94
95  upload_host = '%s.googlecode.com' % project_name
96  upload_uri = '/files'
97  auth_token = base64.b64encode('%s:%s'% (user_name, password))
98  headers = {
99    'Authorization': 'Basic %s' % auth_token,
100    'User-Agent': 'Googlecode.com uploader v0.9.4',
101    'Content-Type': content_type,
102    }
103
104  server = httplib.HTTPSConnection(upload_host)
105  server.request('POST', upload_uri, body, headers)
106  resp = server.getresponse()
107  server.close()
108
109  if resp.status == 201:
110    location = resp.getheader('Location', None)
111  else:
112    location = None
113  return resp.status, resp.reason, location
114
115
116def encode_upload_request(fields, file_path):
117  """Encode the given fields and file into a multipart form body.
118
119  fields is a sequence of (name, value) pairs. file is the path of
120  the file to upload. The file will be uploaded to Google Code with
121  the same file name.
122
123  Returns: (content_type, body) ready for httplib.HTTP instance
124  """
125  BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
126  CRLF = '\r\n'
127
128  body = []
129
130  # Add the metadata about the upload first
131  for key, value in fields:
132    body.extend(
133      ['--' + BOUNDARY,
134       'Content-Disposition: form-data; name="%s"' % key,
135       '',
136       value,
137       ])
138
139  # Now add the file itself
140  file_name = os.path.basename(file_path)
141  f = open(file_path, 'rb')
142  file_content = f.read()
143  f.close()
144
145  body.extend(
146    ['--' + BOUNDARY,
147     'Content-Disposition: form-data; name="filename"; filename="%s"'
148     % file_name,
149     # The upload server determines the mime-type, no need to set it.
150     'Content-Type: application/octet-stream',
151     '',
152     file_content,
153     ])
154
155  # Finalize the form body
156  body.extend(['--' + BOUNDARY + '--', ''])
157
158  return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
159
160
161def upload_find_auth(file_path, project_name, summary, labels=None,
162                     user_name=None, password=None, tries=3):
163  """Find credentials and upload a file to a Google Code project's file server.
164
165  file_path, project_name, summary, and labels are passed as-is to upload.
166
167  Args:
168    file_path: The local path to the file.
169    project_name: The name of your project on Google Code.
170    summary: A small description for the file.
171    labels: an optional list of label strings with which to tag the file.
172    config_dir: Path to Subversion configuration directory, 'none', or None.
173    user_name: Your Google account name.
174    tries: How many attempts to make.
175  """
176  if user_name is None or password is None:
177    from netrc import netrc
178    # Chromium edit: Works on windows without requiring HOME to be set.
179    netrc_path = os.path.join(os.path.expanduser('~'), '.netrc')
180    authenticators = netrc(netrc_path).authenticators("code.google.com")
181    if authenticators:
182      if user_name is None:
183        user_name = authenticators[0]
184      if password is None:
185        password = authenticators[2]
186
187  if user_name is None or password is None:
188    raise RuntimeError('Missing user credentials for upload')
189
190  return upload(file_path, project_name, user_name, password, summary, labels)
191
192
193def main():
194  parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
195                                 '-p PROJECT [options] FILE')
196  parser.add_option('-s', '--summary', dest='summary',
197                    help='Short description of the file')
198  parser.add_option('-p', '--project', dest='project',
199                    help='Google Code project name')
200  parser.add_option('-u', '--user', dest='user',
201                    help='Your Google Code username')
202  parser.add_option('-w', '--password', dest='password',
203                    help='Your Google Code password')
204  parser.add_option('-l', '--labels', dest='labels',
205                    help='An optional list of comma-separated labels to attach '
206                    'to the file')
207
208  options, args = parser.parse_args()
209
210  if not options.summary:
211    parser.error('File summary is missing.')
212  elif not options.project:
213    parser.error('Project name is missing.')
214  elif len(args) < 1:
215    parser.error('File to upload not provided.')
216  elif len(args) > 1:
217    parser.error('Only one file may be specified.')
218
219  file_path = args[0]
220
221  if options.labels:
222    labels = options.labels.split(',')
223  else:
224    labels = None
225
226  status, reason, url = upload_find_auth(file_path, options.project,
227                                         options.summary, labels,
228                                         options.user, options.password)
229  if url:
230    print 'The file was uploaded successfully.'
231    print 'URL: %s' % url
232    return 0
233  else:
234    print 'An error occurred. Your file was not uploaded.'
235    print 'Google Code upload server said: %s (%s)' % (reason, status)
236    return 1
237
238
239if __name__ == '__main__':
240  sys.exit(main())
241