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"""Google Code file uploader script. 47""" 48 49__author__ = 'danderson@google.com (David Anderson)' 50 51import httplib 52import os.path 53import optparse 54import getpass 55import base64 56import sys 57 58 59def 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@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 110def 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 155def 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 209def 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 255if __name__ == '__main__': 256 sys.exit(main()) 257