3 # server.py - part of the FDroid server tools
4 # Copyright (C) 2010-15, Ciaran Gultnieks, ciaran@ciarang.com
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
28 from argparse import ArgumentParser
34 from .exception import FDroidException
39 BINARY_TRANSPARENCY_DIR = 'binary_transparency'
42 def update_awsbucket(repo_section):
44 Upload the contents of the directory `repo_section` (including
45 subdirectories) to the AWS S3 "bucket". The contents of that subdir of the
46 bucket will first be deleted.
48 Requires AWS credentials set in config.py: awsaccesskeyid, awssecretkey
51 logging.debug('Syncing "' + repo_section + '" to Amazon S3 bucket "'
52 + config['awsbucket'] + '"')
54 if common.set_command_in_config('s3cmd'):
55 update_awsbucket_s3cmd(repo_section)
57 update_awsbucket_libcloud(repo_section)
60 def update_awsbucket_s3cmd(repo_section):
61 '''upload using the CLI tool s3cmd, which provides rsync-like sync
63 The upload is done in multiple passes to reduce the chance of
64 interfering with an existing client-server interaction. In the
65 first pass, only new files are uploaded. In the second pass,
66 changed files are uploaded, overwriting what is on the server. On
67 the third/last pass, the indexes are uploaded, and any removed
68 files are deleted from the server. The last pass is the only pass
69 to use a full MD5 checksum of all files to detect changes.
72 logging.debug(_('Using s3cmd to sync with: {url}')
73 .format(url=config['awsbucket']))
75 configfilename = '.s3cfg'
76 fd = os.open(configfilename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0o600)
77 os.write(fd, '[default]\n'.encode('utf-8'))
78 os.write(fd, ('access_key = ' + config['awsaccesskeyid'] + '\n').encode('utf-8'))
79 os.write(fd, ('secret_key = ' + config['awssecretkey'] + '\n').encode('utf-8'))
82 s3bucketurl = 's3://' + config['awsbucket']
83 s3cmd = [config['s3cmd'], '--config=' + configfilename]
84 if subprocess.call(s3cmd + ['info', s3bucketurl]) != 0:
85 logging.warning(_('Creating new S3 bucket: {url}')
86 .format(url=s3bucketurl))
87 if subprocess.call(s3cmd + ['mb', s3bucketurl]) != 0:
88 logging.error(_('Failed to create S3 bucket: {url}')
89 .format(url=s3bucketurl))
90 raise FDroidException()
92 s3cmd_sync = s3cmd + ['sync', '--acl-public']
94 s3cmd_sync += ['--verbose']
96 s3cmd_sync += ['--quiet']
97 indexxml = os.path.join(repo_section, 'index.xml')
98 indexjar = os.path.join(repo_section, 'index.jar')
99 indexv1jar = os.path.join(repo_section, 'index-v1.jar')
101 s3url = s3bucketurl + '/fdroid/'
102 logging.debug('s3cmd sync new files in ' + repo_section + ' to ' + s3url)
103 logging.debug(_('Running first pass with MD5 checking disabled'))
104 if subprocess.call(s3cmd_sync +
105 ['--no-check-md5', '--skip-existing',
106 '--exclude', indexxml,
107 '--exclude', indexjar,
108 '--exclude', indexv1jar,
109 repo_section, s3url]) != 0:
110 raise FDroidException()
111 logging.debug('s3cmd sync all files in ' + repo_section + ' to ' + s3url)
112 if subprocess.call(s3cmd_sync +
114 '--exclude', indexxml,
115 '--exclude', indexjar,
116 '--exclude', indexv1jar,
117 repo_section, s3url]) != 0:
118 raise FDroidException()
120 logging.debug(_('s3cmd sync indexes {path} to {url} and delete')
121 .format(path=repo_section, url=s3url))
122 s3cmd_sync.append('--delete-removed')
123 s3cmd_sync.append('--delete-after')
124 if options.no_checksum:
125 s3cmd_sync.append('--no-check-md5')
127 s3cmd_sync.append('--check-md5')
128 if subprocess.call(s3cmd_sync + [repo_section, s3url]) != 0:
129 raise FDroidException()
132 def update_awsbucket_libcloud(repo_section):
134 Upload the contents of the directory `repo_section` (including
135 subdirectories) to the AWS S3 "bucket". The contents of that subdir of the
136 bucket will first be deleted.
138 Requires AWS credentials set in config.py: awsaccesskeyid, awssecretkey
141 logging.debug(_('using Apache libcloud to sync with {url}')
142 .format(url=config['awsbucket']))
144 import libcloud.security
145 libcloud.security.VERIFY_SSL_CERT = True
146 from libcloud.storage.types import Provider, ContainerDoesNotExistError
147 from libcloud.storage.providers import get_driver
149 if not config.get('awsaccesskeyid') or not config.get('awssecretkey'):
150 raise FDroidException(
151 _('To use awsbucket, awssecretkey and awsaccesskeyid must also be set in config.py!'))
152 awsbucket = config['awsbucket']
154 cls = get_driver(Provider.S3)
155 driver = cls(config['awsaccesskeyid'], config['awssecretkey'])
157 container = driver.get_container(container_name=awsbucket)
158 except ContainerDoesNotExistError:
159 container = driver.create_container(container_name=awsbucket)
160 logging.info(_('Created new container "{name}"')
161 .format(name=container.name))
163 upload_dir = 'fdroid/' + repo_section
165 for obj in container.list_objects():
166 if obj.name.startswith(upload_dir + '/'):
169 for root, dirs, files in os.walk(os.path.join(os.getcwd(), repo_section)):
172 file_to_upload = os.path.join(root, name)
173 object_name = 'fdroid/' + os.path.relpath(file_to_upload, os.getcwd())
174 if object_name not in objs:
177 obj = objs.pop(object_name)
178 if obj.size != os.path.getsize(file_to_upload):
181 # if the sizes match, then compare by MD5
183 with open(file_to_upload, 'rb') as f:
189 if obj.hash != md5.hexdigest():
190 s3url = 's3://' + awsbucket + '/' + obj.name
191 logging.info(' deleting ' + s3url)
192 if not driver.delete_object(obj):
193 logging.warn('Could not delete ' + s3url)
197 logging.debug(' uploading "' + file_to_upload + '"...')
198 extra = {'acl': 'public-read'}
199 if file_to_upload.endswith('.sig'):
200 extra['content_type'] = 'application/pgp-signature'
201 elif file_to_upload.endswith('.asc'):
202 extra['content_type'] = 'application/pgp-signature'
203 logging.info(' uploading ' + os.path.relpath(file_to_upload)
204 + ' to s3://' + awsbucket + '/' + object_name)
205 with open(file_to_upload, 'rb') as iterator:
206 obj = driver.upload_object_via_stream(iterator=iterator,
208 object_name=object_name,
210 # delete the remnants in the bucket, they do not exist locally
212 object_name, obj = objs.popitem()
213 s3url = 's3://' + awsbucket + '/' + object_name
214 if object_name.startswith(upload_dir):
215 logging.warn(' deleting ' + s3url)
216 driver.delete_object(obj)
218 logging.info(' skipping ' + s3url)
221 def update_serverwebroot(serverwebroot, repo_section):
222 # use a checksum comparison for accurate comparisons on different
223 # filesystems, for example, FAT has a low resolution timestamp
224 rsyncargs = ['rsync', '--archive', '--delete-after', '--safe-links']
225 if not options.no_checksum:
226 rsyncargs.append('--checksum')
228 rsyncargs += ['--verbose']
230 rsyncargs += ['--quiet']
231 if options.identity_file is not None:
232 rsyncargs += ['-e', 'ssh -oBatchMode=yes -oIdentitiesOnly=yes -i ' + options.identity_file]
233 elif 'identity_file' in config:
234 rsyncargs += ['-e', 'ssh -oBatchMode=yes -oIdentitiesOnly=yes -i ' + config['identity_file']]
235 indexxml = os.path.join(repo_section, 'index.xml')
236 indexjar = os.path.join(repo_section, 'index.jar')
237 indexv1jar = os.path.join(repo_section, 'index-v1.jar')
238 # Upload the first time without the index files and delay the deletion as
239 # much as possible, that keeps the repo functional while this update is
240 # running. Then once it is complete, rerun the command again to upload
241 # the index files. Always using the same target with rsync allows for
242 # very strict settings on the receiving server, you can literally specify
243 # the one rsync command that is allowed to run in ~/.ssh/authorized_keys.
244 # (serverwebroot is guaranteed to have a trailing slash in common.py)
245 logging.info('rsyncing ' + repo_section + ' to ' + serverwebroot)
246 if subprocess.call(rsyncargs +
247 ['--exclude', indexxml, '--exclude', indexjar,
248 '--exclude', indexv1jar,
249 repo_section, serverwebroot]) != 0:
250 raise FDroidException()
251 if subprocess.call(rsyncargs + [repo_section, serverwebroot]) != 0:
252 raise FDroidException()
253 # upload "current version" symlinks if requested
254 if config['make_current_version_link'] and repo_section == 'repo':
256 for f in glob.glob('*.apk') \
257 + glob.glob('*.apk.asc') + glob.glob('*.apk.sig'):
258 if os.path.islink(f):
259 links_to_upload.append(f)
260 if len(links_to_upload) > 0:
261 if subprocess.call(rsyncargs + links_to_upload + [serverwebroot]) != 0:
262 raise FDroidException()
265 def _local_sync(fromdir, todir):
266 rsyncargs = ['rsync', '--recursive', '--safe-links', '--times', '--perms',
267 '--one-file-system', '--delete', '--chmod=Da+rx,Fa-x,a+r,u+w']
268 # use stricter rsync checking on all files since people using offline mode
269 # are already prioritizing security above ease and speed
270 if not options.no_checksum:
271 rsyncargs.append('--checksum')
273 rsyncargs += ['--verbose']
275 rsyncargs += ['--quiet']
276 logging.debug(' '.join(rsyncargs + [fromdir, todir]))
277 if subprocess.call(rsyncargs + [fromdir, todir]) != 0:
278 raise FDroidException()
281 def sync_from_localcopy(repo_section, local_copy_dir):
282 '''Syncs the repo from "local copy dir" filesystem to this box
284 In setups that use offline signing, this is the last step that
285 syncs the repo from the "local copy dir" e.g. a thumb drive to the
286 repo on the local filesystem. That local repo is then used to
287 push to all the servers that are configured.
290 logging.info('Syncing from local_copy_dir to this repo.')
291 # trailing slashes have a meaning in rsync which is not needed here, so
292 # make sure both paths have exactly one trailing slash
293 _local_sync(os.path.join(local_copy_dir, repo_section).rstrip('/') + '/',
294 repo_section.rstrip('/') + '/')
296 offline_copy = os.path.join(local_copy_dir, BINARY_TRANSPARENCY_DIR)
297 if os.path.exists(os.path.join(offline_copy, '.git')):
298 online_copy = os.path.join(os.getcwd(), BINARY_TRANSPARENCY_DIR)
299 push_binary_transparency(offline_copy, online_copy)
302 def update_localcopy(repo_section, local_copy_dir):
303 '''copy data from offline to the "local copy dir" filesystem
305 This updates the copy of this repo used to shuttle data from an
306 offline signing machine to the online machine, e.g. on a thumb
310 # local_copy_dir is guaranteed to have a trailing slash in main() below
311 _local_sync(repo_section, local_copy_dir)
313 offline_copy = os.path.join(os.getcwd(), BINARY_TRANSPARENCY_DIR)
314 if os.path.isdir(os.path.join(offline_copy, '.git')):
315 online_copy = os.path.join(local_copy_dir, BINARY_TRANSPARENCY_DIR)
316 push_binary_transparency(offline_copy, online_copy)
319 def _get_size(start_path='.'):
320 '''get size of all files in a dir https://stackoverflow.com/a/1392549'''
322 for root, dirs, files in os.walk(start_path):
324 fp = os.path.join(root, f)
325 total_size += os.path.getsize(fp)
329 def update_servergitmirrors(servergitmirrors, repo_section):
330 '''update repo mirrors stored in git repos
332 This is a hack to use public git repos as F-Droid repos. It
333 recreates the git repo from scratch each time, so that there is no
334 history. That keeps the size of the git repo small. Services
335 like GitHub or GitLab have a size limit of something like 1 gig.
336 This git repo is only a git repo for the purpose of being hosted.
337 For history, there is the archive section, and there is the binary
342 from clint.textui import progress
343 if config.get('local_copy_dir') \
344 and not config.get('sync_from_local_copy_dir'):
345 logging.debug('Offline machine, skipping git mirror generation until `fdroid server update`')
348 # right now we support only 'repo' git-mirroring
349 if repo_section == 'repo':
350 git_mirror_path = 'git-mirror'
351 dotgit = os.path.join(git_mirror_path, '.git')
352 git_repodir = os.path.join(git_mirror_path, 'fdroid', repo_section)
353 if not os.path.isdir(git_repodir):
354 os.makedirs(git_repodir)
355 if os.path.isdir(dotgit) and _get_size(git_mirror_path) > 1000000000:
356 logging.warning('Deleting git-mirror history, repo is too big (1 gig max)')
357 shutil.rmtree(dotgit)
359 # rsync is very particular about trailing slashes
360 _local_sync(repo_section.rstrip('/') + '/', git_repodir.rstrip('/') + '/')
362 # use custom SSH command if identity_file specified
363 ssh_cmd = 'ssh -oBatchMode=yes'
364 if options.identity_file is not None:
365 ssh_cmd += ' -oIdentitiesOnly=yes -i "%s"' % options.identity_file
366 elif 'identity_file' in config:
367 ssh_cmd += ' -oIdentitiesOnly=yes -i "%s"' % config['identity_file']
369 repo = git.Repo.init(git_mirror_path)
371 for remote_url in servergitmirrors:
372 hostname = re.sub(r'\W*\w+\W+(\w+).*', r'\1', remote_url)
373 r = git.remote.Remote(repo, hostname)
374 if r in repo.remotes:
375 r = repo.remote(hostname)
376 if 'set_url' in dir(r): # force remote URL if using GitPython 2.x
377 r.set_url(remote_url)
379 repo.create_remote(hostname, remote_url)
380 logging.info('Mirroring to: ' + remote_url)
382 # sadly index.add don't allow the --all parameter
383 logging.debug('Adding all files to git mirror')
384 repo.git.add(all=True)
385 logging.debug('Committing all files into git mirror')
386 repo.index.commit("fdroidserver git-mirror")
391 class MyProgressPrinter(git.RemoteProgress):
392 def update(self, op_code, current, maximum=None, message=None):
393 if isinstance(maximum, float):
394 bar.show(current, maximum)
395 progress = MyProgressPrinter()
399 # push for every remote. This will overwrite the git history
400 for remote in repo.remotes:
401 if remote.name == 'gitlab':
402 logging.debug('Writing .gitlab-ci.yml to deploy to GitLab Pages')
403 with open(os.path.join(git_mirror_path, ".gitlab-ci.yml"), "wt") as out_file:
404 out_file.write("""pages:
414 repo.git.add(all=True)
415 repo.index.commit("fdroidserver git-mirror: Deploy to GitLab Pages")
417 logging.debug(_('Pushing to {url}').format(url=remote.url))
418 with repo.git.custom_environment(GIT_SSH_COMMAND=ssh_cmd):
419 pushinfos = remote.push('master', force=True, set_upstream=True, progress=progress)
420 for pushinfo in pushinfos:
421 if pushinfo.flags & (git.remote.PushInfo.ERROR
422 | git.remote.PushInfo.REJECTED
423 | git.remote.PushInfo.REMOTE_FAILURE
424 | git.remote.PushInfo.REMOTE_REJECTED):
425 raise FDroidException(remote.url + ' push failed: ' + str(pushinfo.flags)
426 + ' ' + pushinfo.summary)
428 logging.debug(remote.url + ': ' + pushinfo.summary)
434 def upload_to_android_observatory(repo_section):
435 # depend on requests and lxml only if users enable AO
437 from lxml.html import fromstring
439 if repo_section == 'repo':
440 for f in glob.glob(os.path.join(repo_section, '*.apk')):
442 fname = os.path.basename(f)
443 logging.info('Uploading ' + fname + ' to androidobservatory.org')
445 # upload the file with a post request
446 r = requests.post('https://androidobservatory.org/upload', files={'apk': (fname, open(fpath, 'rb'))})
450 # from now on XPath will be used to retrieve the message in the HTML
451 # androidobservatory doesn't have a nice API to talk with
452 # so we must scrape the page content
453 tree = fromstring(response)
454 alert = tree.xpath("//html/body/div[@class='container content-container']/div[@class='alert alert-info']")[0]
459 # if the application was added successfully we retrive the url
460 # if the application was already uploaded we use the redirect page url
461 if el.attrib.get("href") is not None:
462 appurl = page + el.attrib["href"][1:]
463 message += el.text.replace(" here", "") + el.tail
466 message = message.strip() + " " + appurl
467 logging.info(message)
470 def upload_to_virustotal(repo_section, vt_apikey):
474 logging.getLogger("urllib3").setLevel(logging.WARNING)
475 logging.getLogger("requests").setLevel(logging.WARNING)
477 if repo_section == 'repo':
478 if not os.path.exists('virustotal'):
479 os.mkdir('virustotal')
480 with open(os.path.join(repo_section, 'index-v1.json')) as fp:
481 index = json.load(fp)
482 for packageName, packages in index['packages'].items():
483 for package in packages:
484 outputfilename = os.path.join('virustotal',
485 packageName + '_' + str(package.get('versionCode'))
486 + '_' + package['hash'] + '.json')
487 if os.path.exists(outputfilename):
488 logging.debug(package['apkName'] + ' results are in ' + outputfilename)
490 filename = package['apkName']
491 repofilename = os.path.join(repo_section, filename)
492 logging.info('Checking if ' + repofilename + ' is on virustotal')
495 "User-Agent": "F-Droid"
499 'resource': package['hash'],
501 needs_file_upload = False
503 r = requests.post('https://www.virustotal.com/vtapi/v2/file/report',
504 params=params, headers=headers)
505 if r.status_code == 200:
507 if response['response_code'] == 0:
508 needs_file_upload = True
510 response['filename'] = filename
511 response['packageName'] = packageName
512 response['versionCode'] = package.get('versionCode')
513 response['versionName'] = package.get('versionName')
514 with open(outputfilename, 'w') as fp:
515 json.dump(response, fp, indent=2, sort_keys=True)
517 if response.get('positives') > 0:
518 logging.warning(repofilename + ' has been flagged by virustotal '
519 + str(response['positives']) + ' times:'
520 + '\n\t' + response['permalink'])
522 elif r.status_code == 204:
523 time.sleep(10) # wait for public API rate limiting
525 if needs_file_upload:
526 logging.info('Uploading ' + repofilename + ' to virustotal')
528 'file': (filename, open(repofilename, 'rb'))
530 r = requests.post('https://www.virustotal.com/vtapi/v2/file/scan',
531 params=params, headers=headers, files=files)
534 logging.info(response['verbose_msg'] + " " + response['permalink'])
537 def push_binary_transparency(git_repo_path, git_remote):
538 '''push the binary transparency git repo to the specifed remote.
540 If the remote is a local directory, make sure it exists, and is a
541 git repo. This is used to move this git repo from an offline
542 machine onto a flash drive, then onto the online machine. Also,
543 this pulls because pushing to a non-bare git repo is error prone.
545 This is also used in offline signing setups, where it then also
546 creates a "local copy dir" git repo that serves to shuttle the git
547 data from the offline machine to the online machine. In that
548 case, git_remote is a dir on the local file system, e.g. a thumb
554 logging.info(_('Pushing binary transparency log to {url}')
555 .format(url=git_remote))
557 if os.path.isdir(os.path.dirname(git_remote)):
558 # from offline machine to thumbdrive
559 remote_path = os.path.abspath(git_repo_path)
560 if not os.path.isdir(os.path.join(git_remote, '.git')):
561 os.makedirs(git_remote, exist_ok=True)
562 thumbdriverepo = git.Repo.init(git_remote)
563 local = thumbdriverepo.create_remote('local', remote_path)
565 thumbdriverepo = git.Repo(git_remote)
566 local = git.remote.Remote(thumbdriverepo, 'local')
567 if local in thumbdriverepo.remotes:
568 local = thumbdriverepo.remote('local')
569 if 'set_url' in dir(local): # force remote URL if using GitPython 2.x
570 local.set_url(remote_path)
572 local = thumbdriverepo.create_remote('local', remote_path)
575 # from online machine to remote on a server on the internet
576 gitrepo = git.Repo(git_repo_path)
577 origin = git.remote.Remote(gitrepo, 'origin')
578 if origin in gitrepo.remotes:
579 origin = gitrepo.remote('origin')
580 if 'set_url' in dir(origin): # added in GitPython 2.x
581 origin.set_url(git_remote)
583 origin = gitrepo.create_remote('origin', git_remote)
584 origin.push('master')
588 global config, options
590 # Parse command line...
591 parser = ArgumentParser()
592 common.setup_global_opts(parser)
593 parser.add_argument("command", help=_("command to execute, either 'init' or 'update'"))
594 parser.add_argument("-i", "--identity-file", default=None,
595 help=_("Specify an identity file to provide to SSH for rsyncing"))
596 parser.add_argument("--local-copy-dir", default=None,
597 help=_("Specify a local folder to sync the repo to"))
598 parser.add_argument("--no-checksum", action="store_true", default=False,
599 help=_("Don't use rsync checksums"))
600 options = parser.parse_args()
602 config = common.read_config(options)
604 if options.command != 'init' and options.command != 'update':
605 logging.critical(_("The only commands currently supported are 'init' and 'update'"))
608 if config.get('nonstandardwebroot') is True:
609 standardwebroot = False
611 standardwebroot = True
613 for serverwebroot in config.get('serverwebroot', []):
614 # this supports both an ssh host:path and just a path
615 s = serverwebroot.rstrip('/').split(':')
621 logging.error(_('Malformed serverwebroot line:') + ' ' + serverwebroot)
623 repobase = os.path.basename(fdroiddir)
624 if standardwebroot and repobase != 'fdroid':
625 logging.error('serverwebroot path does not end with "fdroid", '
626 + 'perhaps you meant one of these:\n\t'
627 + serverwebroot.rstrip('/') + '/fdroid\n\t'
628 + serverwebroot.rstrip('/').rstrip(repobase) + 'fdroid')
631 if options.local_copy_dir is not None:
632 local_copy_dir = options.local_copy_dir
633 elif config.get('local_copy_dir'):
634 local_copy_dir = config['local_copy_dir']
636 local_copy_dir = None
637 if local_copy_dir is not None:
638 fdroiddir = local_copy_dir.rstrip('/')
639 if os.path.exists(fdroiddir) and not os.path.isdir(fdroiddir):
640 logging.error(_('local_copy_dir must be directory, not a file!'))
642 if not os.path.exists(os.path.dirname(fdroiddir)):
643 logging.error(_('The root dir for local_copy_dir "{path}" does not exist!')
644 .format(path=os.path.dirname(fdroiddir)))
646 if not os.path.isabs(fdroiddir):
647 logging.error(_('local_copy_dir must be an absolute path!'))
649 repobase = os.path.basename(fdroiddir)
650 if standardwebroot and repobase != 'fdroid':
651 logging.error(_('local_copy_dir does not end with "fdroid", '
652 + 'perhaps you meant: "{path}"')
653 .format(path=fdroiddir + '/fdroid'))
655 if local_copy_dir[-1] != '/':
656 local_copy_dir += '/'
657 local_copy_dir = local_copy_dir.replace('//', '/')
658 if not os.path.exists(fdroiddir):
661 if not config.get('awsbucket') \
662 and not config.get('serverwebroot') \
663 and not config.get('servergitmirrors') \
664 and not config.get('androidobservatory') \
665 and not config.get('binary_transparency_remote') \
666 and not config.get('virustotal_apikey') \
667 and local_copy_dir is None:
668 logging.warn(_('No option set! Edit your config.py to set at least one of these:')
669 + '\nserverwebroot, servergitmirrors, local_copy_dir, awsbucket, virustotal_apikey, androidobservatory, or binary_transparency_remote')
672 repo_sections = ['repo']
673 if config['archive_older'] != 0:
674 repo_sections.append('archive')
675 if not os.path.exists('archive'):
677 if config['per_app_repos']:
678 repo_sections += common.get_per_app_repos()
680 if options.command == 'init':
681 ssh = paramiko.SSHClient()
682 ssh.load_system_host_keys()
683 for serverwebroot in config.get('serverwebroot', []):
684 sshstr, remotepath = serverwebroot.rstrip('/').split(':')
685 if sshstr.find('@') >= 0:
686 username, hostname = sshstr.split('@')
688 username = pwd.getpwuid(os.getuid())[0] # get effective uid
690 ssh.connect(hostname, username=username)
691 sftp = ssh.open_sftp()
692 if os.path.basename(remotepath) \
693 not in sftp.listdir(os.path.dirname(remotepath)):
694 sftp.mkdir(remotepath, mode=0o755)
695 for repo_section in repo_sections:
696 repo_path = os.path.join(remotepath, repo_section)
697 if os.path.basename(repo_path) \
698 not in sftp.listdir(remotepath):
699 sftp.mkdir(repo_path, mode=0o755)
702 elif options.command == 'update':
703 for repo_section in repo_sections:
704 if local_copy_dir is not None:
705 if config['sync_from_local_copy_dir']:
706 sync_from_localcopy(repo_section, local_copy_dir)
708 update_localcopy(repo_section, local_copy_dir)
709 for serverwebroot in config.get('serverwebroot', []):
710 update_serverwebroot(serverwebroot, repo_section)
711 if config.get('servergitmirrors', []):
712 # update_servergitmirrors will take care of multiple mirrors so don't need a foreach
713 servergitmirrors = config.get('servergitmirrors', [])
714 update_servergitmirrors(servergitmirrors, repo_section)
715 if config.get('awsbucket'):
716 update_awsbucket(repo_section)
717 if config.get('androidobservatory'):
718 upload_to_android_observatory(repo_section)
719 if config.get('virustotal_apikey'):
720 upload_to_virustotal(repo_section, config.get('virustotal_apikey'))
722 binary_transparency_remote = config.get('binary_transparency_remote')
723 if binary_transparency_remote:
724 push_binary_transparency(BINARY_TRANSPARENCY_DIR,
725 binary_transparency_remote)
730 if __name__ == "__main__":