# a trigger to create and save torrents to an nfs share # Copyright (C) 2015 Forrest S # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import errno import base64 import sys import deluge.maketorrent from deluge.ui.client import client from twisted.internet import reactor, defer from twisted.python import log from deluge.log import setupLogger USERNAME="user" PASSWORD="password" setupLogger() # log.startLogging(sys.stdout) try: import P4 as p4 except ImportError, e: import p4 def run(serverport, changelist): torrent_info, errs = create_torrents(serverport, changelist) if errs: log.msg( '\n'.join(errs)) if torrent_info: begin_seeding_torrent(torrent_info) def create_torrents(serverport, changelist): trackers = os.getenv('P4TRACKERS') if not trackers: trackers = [["http://127.0.0.1:6969/announce"]] else: trackers = [trackers.split(':'),] torrent_storage_root = os.getenv('P4TORRENT') if not torrent_storage_root: torrent_storage_root = os.getcwd() + "p4torrents" p4c = connect_to_p4(serverport,ntries = 120,delay = 10) # get change info for changelist change_info = p4c.run_describe("-s", changelist) if not change_info: raise P4SessionError("change %s appears not to exist" % change_number) depot = "" result = change_info[0] depotFiles = result.get('depotFile', []) depotSets = set(get_depot(dp) for dp in depotFiles) if len(depotSets) == 1: temp_depot = depotSets.pop() if not depot: depot = temp_depot if not depot.startswith("//"): depot = "//" + depot file_actions = {} #retrieve actions for i, depotFile in enumerate(depotFiles): file_actions[depotFile] = result['action'][i] # if no actions were taken, return if not file_actions: return description = result['desc'] client = result['client'] user = result['user'] depot = depot[2:] if not depot: depot = "unknown" errs = [] torrent_info = [] # get fstat for files fstat = get_fstat(p4c, file_actions.keys(), changelist) #deal with fstats for fstat_entry in fstat: if not 'headRev' in fstat_entry: continue depot_path = fstat_entry['lbrFile'] if not is_stored_in_full(fstat_entry['headType']): msg = """Must store file in full, perhaps you should run the commands \tp4 edit %s \tp4 reopen -t +F %s""" % (depot_path, depot_path) errs += [msg] continue version_number = fstat_entry['headRev'] if depot_path.startswith("//"): depot_path = depot_path[2:] torrent_storage_path = os.path.join(torrent_storage_root,depot_path) ## deal with purge revisions? source = fstat_entry['lbrFile'] lbrRev = fstat_entry['lbrRev'] # get repo file path rep_file_path = get_rep_file_path(p4c, source, lbrRev) revision_num = fstat_entry['headRev'] # construct the destination path for torrent corresponding to the changelist and revision number of given file # //depot/path/to/file.txt -> P4TORRENT/depot/path/to/file.txt/file.txt.torrent@CHANGELIST revision_path, changelist_path = create_revision_and_changelist_path(torrent_storage_path, revision_num, changelist) # perforce stores files as filename,d/lbrRev this is distinctly not useful for torrents, because the file downloaded # shares the name of the file for the original torrent. thus a link to the revision, stored as such is created # repfilepath/path/to/file.txt,d/lbrRev -> P4TORRENT/depot/path/to/file.txt/changelist/file.txt new_data_path, err = make_new_data_path(rep_file_path, torrent_storage_path, changelist, os.path.basename(torrent_storage_path)) if err: errs +=[err] continue torrent = deluge.maketorrent.TorrentMetadata() torrent.data_path = new_data_path torrent.comment = description torrent.trackers = trackers try_to_save_torrent(torrent, changelist_path) try: link_or_symlink(changelist_path, revision_path) except OSError, e: msg = "Encountered '%s' while trying to link \n\t'%s' \n\t'%s'" %(str(e), changelist_path, revision_path) errs += [msg] continue # deluge requires tuples (path, b64encoded torrent file, options) # immediately seed with open(revision_path) as f: data = base64.b64encode(f.read()) torrent_info+= [(revision_path, data, {"download_location": os.path.dirname(new_data_path), "auto_managed":True,"super_seeding":True, "seed_mode":True}) ] return torrent_info, errs def make_new_data_path(rep_file_path, torrent_storage_path, changelist, filename): path = os.path.join(torrent_storage_path +"@"+ str(changelist),filename) os.makedirs(os.path.dirname(path)) msg = "" try: link_or_symlink(rep_file_path, path) except OSError, e: msg = "Encountered '%s' while trying to link \n\t'%s' \n\t'%s'" %(str(e), rep_file_path, path) return path, msg # adds torrents def begin_seeding_torrent(torrent_info): setupLogger() deferred_object = client.connect(username=USERNAME,password=PASSWORD) def on_connect_success(result): torrent_ids = [] for torrent in torrent_info: torrent_id = client.core.add_torrent_file(torrent[0], torrent[1], torrent[2]) torrent_ids += [torrent_id] deferred_list = defer.DeferredList(torrent_ids) def log(torrent_ids): pass deferred_list.addCallback(log) return deferred_list def shutdown(s): client.disconnect() reactor.stop() def on_connect_fail(result): print "Failed to connect" print result reactor.stop() deferred_object.addCallback(on_connect_success) deferred_object.addErrback(on_connect_fail) deferred_object.addBoth(shutdown) reactor.run() def connect_to_p4(serverport,ntries,delay): p4c = p4.P4() p4c.port = serverport p4c.cwd = os.getcwd() ex = None while ntries >= 0: try: p4c.connect() return p4c except p4.P4Exception, ex: ntries -=1 time.sleep(delay) else: raise ex def get_fstat(p4c, fstat_args, changelist): # Get info pertaining to the current change. fstat_args.insert(0, "-Ocl") fstat_args.insert(0, "-e") fstat_args.insert(1, changelist) fstat = p4c.run_fstat(fstat_args) return fstat def get_field_from_fstat(fstat, field): field = dict((revision['depotFile'], revision[field]) for revision in fstat if field in revision) return field def get_depot(depot_path): """Return the depot name for a given depotPath (i.e. 'abc' on input '//abc/def'). depotPath must begin with // and have length 3 or greater. """ index = depot_path.find('/', 2) return (index < 0 and depot_path[2:]) or depot_path[2:index] def get_rep_file_path(p4c, depot_path, lbrRev): """Construct the path to the file for depotPath, given the lbrRev. You must have an actively connected p4 session for this call to succeed. """ depot = get_depot(depot_path) server_root = p4c.run_info()[0]['serverRoot'] depot_root = os.path.realpath(os.path.join(server_root, depot)) return os.path.join(depot_root, depot_path[len(depot) + 3:] + ",d", lbrRev) _typeAliases = { "ctext" : "text+C", "cxtext" : "text+Cx", "ktext" : "text+k", "kxtext" : "text+kx", "ltext" : "text+F", "tempobj" : "binary+FSw", "ubinary" : "binary+F", "uresource" : "resource+F", "uxbinary" : "binary+Fx", "xbinary" : "binary+x", "xltext" : "text+Fx", "xtempobj" : "binary+Swx", "xtext" : "text+x", "xunicode" : "unicode+x", "xutf16" : "utf16+x", } def is_stored_in_full(file_type): """Return true if the file_type indicates a binary/fully stored file""" fields = _typeAliases.get(file_type, file_type).split('+') return len(fields) == 2 and 'F' in fields[1] def is_symlink_file_type(file_type): """Return true if the fileType indicates a symlink type.""" return file_type.startswith("symlink") def create_revision_and_changelist_path(basepath, revision_num, changelist): head, tail = os.path.split(basepath) revision_path = os.path.join(head,tail,tail+".torrent#"+revision_num) changelist_path = os.path.join(head,tail,tail+".torrent@"+str(changelist)) return revision_path, changelist_path def try_to_save_torrent(torrent_metadata=None, changelist_path=""): if not torrent_metadata: raise ValueError("Missing torrent metadata") try: torrent_metadata.save(changelist_path) except IOError, e: try: os.makedirs(os.path.dirname(changelist_path)) except OSError: pass try: torrent_metadata.save(changelist_path) except: raise e # tries to hardlink, otherwise symlinks def link_or_symlink(src, target): try: return os.link(src, target) except OSError, e: if e.errno == errno.EXDEV: return os.symlink(src, target) raise e