Source code for pyfilemail.transfer

import os
import datetime
from hashlib import md5
from uuid import uuid4
from mimetypes import guess_type
from zipfile import ZipFile

from clint.textui.progress import Bar as ProgressBar
from requests_toolbelt.multipart import encoder

import users
import pyfilemail as pm
from urls import get_URL
from functools import wraps
from pyfilemail import logger, login_required
from errors import hellraiser, FMBaseError, FMFileError


# Decorator to make sure we don't transfer complete packages twice
def not_completed(f):
    """Decorator function to check if user is loged in.

    :raises: :class:`FMBaseError` if not logged in
    """

    @wraps(f)
    def check_if_complete(cls, *args, **kwargs):
        if cls.is_complete:
            raise FMBaseError('Transfer already completed.')

        return f(cls, *args, **kwargs)

    return check_if_complete


[docs]class Transfer(object): """This is is the gateway to sending and recieving files through filemail. :param fm_user: username :param to: recipient(s) :param subject: :param message: :param notify: Notify when recipient(s) download files :param confirmation: Receive confirmation email when files are uploaded :param days: Number of days files are available for download :param downloads: Number of times files may be downloaded :param password: Protect download with given password :param checksum: Create checksum of added files (a bit slower process) :param zip_: Compress files in a zip file before sending :type zip_: bool :type checksum: bool :type password: str, unicode :type days: int :type confirmation: bool :type notify: bool :type message: str, unicode :type fm_user: :class:`pyfilemail.User`, str :type to: str, list :type subject: str, unicode """ def __init__(self, fm_user, to=None, subject=None, message=None, notify=False, confirmation=False, days=3, downloads=0, password=None, checksum=True, zip_=False, _restore=False): if isinstance(fm_user, basestring): self.fm_user = users.User(fm_user) elif isinstance(fm_user, users.User): self.fm_user = fm_user else: raise FMBaseError('fm_user must be of type "string or User"') # Add transfer to user's transfer list self.fm_user.transfers.append(self) self._files = [] self._complete = False self.checksum = checksum self.zip_ = zip_ self.config = self.fm_user.config self.session = self.fm_user.session self.transfer_info = { 'from': self.fm_user.username, 'to': self._parse_recipients(to), 'subject': subject, 'message': message, 'notify': notify, 'confirmation': confirmation, 'days': days, 'downloads': downloads, 'password': password } if not _restore: self._initialize() def _initialize(self): """Initialize transfer.""" payload = { 'apikey': self.session.cookies.get('apikey'), 'source': self.session.cookies.get('source') } if self.fm_user.logged_in: payload['logintoken'] = self.session.cookies.get('logintoken') payload.update(self.transfer_info) method, url = get_URL('init') res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: for key in ['transferid', 'transferkey', 'transferurl']: self.transfer_info[key] = res.json().get(key) else: hellraiser(res) @property def logged_in(self): """If registered user is logged in or not. :rtype: bool """ return self.session.cookies.get('logintoken') and True or False def _parse_recipients(self, to): """Make sure we have a "," separated list of recipients :param to: Recipient(s) :type to: (str, list, :class:`pyfilemail.Contact`, :class:`pyfilemail.Group` ) :rtype: ``str`` """ if to is None: return None if isinstance(to, list): recipients = [] for recipient in to: if isinstance(recipient, dict): if 'contactgroupname' in recipient: recipients.append(recipient['contactgroupname']) else: recipients.append(recipient.get('email')) else: recipients.append(recipient) elif isinstance(to, basestring): if ',' in to: recipients = to.strip().split(',') else: recipients = [to] return ', '.join(recipients)
[docs] def add_files(self, files): """Add files and/or folders to transfer. If :class:`Transfer.compress` attribute is set to ``True``, files will get packed into a zip file before sending. :param files: Files or folders to send :type files: str, list """ if isinstance(files, basestring): files = [files] zip_file = None if self.zip_: zip_filename = self._get_zip_filename() zip_file = ZipFile(zip_filename, 'w') for filename in files: if os.path.isdir(filename): for dirname, subdirs, filelist in os.walk(filename): if dirname: if self.zip_: zip_file.write(dirname) for fname in filelist: filepath = os.path.join(dirname, fname) if self.zip_: zip_file.write(filepath) else: fmfile = self.get_file_specs(filepath, keep_folders=True) if fmfile['totalsize'] > 0: self._files.append(fmfile) else: if self.zip_: zip_file.write(filename) else: fmfile = self.get_file_specs(filename) self._files.append(fmfile) if self.zip_: zip_file.close() filename = zip_filename fmfile = self.get_file_specs(filename) self._files.append(fmfile)
@property def files(self): """:returns: List of files/folders added to transfer :rtype: ``list`` """ return self._files
[docs] def get_file_specs(self, filepath, keep_folders=False): """Gather information on files needed for valid transfer. :param filepath: Path to file in question :param keep_folders: Whether or not to maintain folder structure :type keep_folders: bool :type filepath: str, unicode :rtype: ``dict`` """ path, filename = os.path.split(filepath) fileid = str(uuid4()).replace('-', '') if self.checksum: with open(filepath, 'rb') as f: md5hash = md5(f.read()).digest().encode('base64')[:-1] else: md5hash = None specs = { 'transferid': self.transfer_id, 'transferkey': self.transfer_info['transferkey'], 'fileid': fileid, 'filepath': filepath, 'thefilename': keep_folders and filepath or filename, 'totalsize': os.path.getsize(filepath), 'md5': md5hash, 'content-type': guess_type(filepath)[0] } return specs
[docs] def get_files(self): """Get information on file in transfer from Filemail. :rtype: ``list`` of ``dict`` objects with info on files """ method, url = get_URL('get') payload = { 'apikey': self.session.cookies.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'transferid': self.transfer_id, } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: transfer_data = res.json()['transfer'] files = transfer_data['files'] for file_data in files: self._files.append(file_data) return self.files hellraiser(res)
def _get_zip_filename(self): """Create a filename for zip file when :class:Transfer.compress is set to ``True`` :rtype: str """ date = datetime.datetime.now().strftime('%Y_%m_%d-%H%M%S') zip_file = 'filemail_transfer_{date}.zip'.format(date=date) return zip_file @not_completed
[docs] def send(self, auto_complete=True, callback=None): """Begin uploading file(s) and sending email(s). If `auto_complete` is set to ``False`` you will have to call the :func:`Transfer.complete` function at a later stage. :param auto_complete: Whether or not to mark transfer as complete and send emails to recipient(s) :param callback: Callback function which will receive total file size and bytes read as arguments :type auto_complete: ``bool`` :type callback: ``func`` """ tot = len(self.files) url = self.transfer_info['transferurl'] for index, fmfile in enumerate(self.files): msg = 'Uploading: "{filename}" ({cur}/{tot})' logger.debug( msg.format( filename=fmfile['thefilename'], cur=index + 1, tot=tot) ) with open(fmfile['filepath'], 'rb') as file_obj: fields = { fmfile['thefilename']: ( 'filename', file_obj, fmfile['content-type'] ) } def pg_callback(monitor): if pm.COMMANDLINE: bar.show(monitor.bytes_read) elif callback is not None: callback(fmfile['totalsize'], monitor.bytes_read) m_encoder = encoder.MultipartEncoder(fields=fields) monitor = encoder.MultipartEncoderMonitor(m_encoder, pg_callback ) label = fmfile['thefilename'] + ': ' if pm.COMMANDLINE: bar = ProgressBar(label=label, expected_size=fmfile['totalsize']) headers = {'Content-Type': m_encoder.content_type} res = self.session.post(url, params=fmfile, data=monitor, headers=headers) if res.status_code != 200: hellraiser(res) #logger.info('\r') if auto_complete: return self.complete() return res
[docs] def complete(self): """Completes the transfer and shoots off email(s) to recipient(s).""" method, url = get_URL('complete') payload = { 'apikey': self.session.cookies.get('apikey'), 'transferid': self.transfer_id, 'transferkey': self.transfer_info['transferkey'] } res = getattr(self.session, method)(url, params=payload) if res.status_code != 200: hellraiser(res) self._complete = True return res
@property def is_complete(self): """:rtype: ``bool`` ``True`` if transfer is complete""" if 'status' in self.transfer_info: self._complete = self.transfer_info['status'] == 'STATUS_COMPLETE' return self._complete @property def transfer_id(self): """ Get the transfer id for the current Transfer. :rtype: ``unicode`` with transfer id """ if 'transferid' in self.transfer_info: return self.transfer_info['transferid'] return self.transfer_info['id']
[docs] def forward(self, to): """Forward prior transfer to new recipient(s). :param to: new recipients to a previous transfer. Use ``list`` or comma seperatde ``str`` or ``unicode`` list :type to: ``list`` or ``str`` or ``unicode`` :rtype: ``bool`` """ method, url = get_URL('forward') payload = { 'apikey': self.session.cookies.get('apikey'), 'transferid': self.transfer_id, 'transferkey': self.transfer_info.get('transferkey'), 'to': self._parse_recipients(to) } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: return True hellraiser(res)
@login_required
[docs] def share(self, to, sender=None, message=None): """Share transfer with new message to new people. :param to: receiver(s) :param sender: Alternate email address as sender :param message: Meggase to new recipients :type to: ``list`` or ``str`` or ``unicode`` :type sender: ``str`` or ``unicode`` :type message: ``str`` or ``unicode`` :rtyep: ``bool`` """ method, url = get_URL('share') payload = { 'apikey': self.session.cookies.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'transferid': self.transfer_id, 'to': self._parse_recipients(to), 'from': sender or self.fm_user.username, 'message': message or '' } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: return True hellraiser(res)
[docs] def cancel(self): """Cancel the current transfer. :rtype: ``bool`` """ method, url = get_URL('cancel') payload = { 'apikey': self.config.get('apikey'), 'transferid': self.transfer_id, 'transferkey': self.transfer_info.get('transferkey') } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: self._complete = True return True hellraiser(res)
@login_required
[docs] def delete(self): """Delete the current transfer. :rtype: ``bool`` """ method, url = get_URL('delete') payload = { 'apikey': self.config.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'transferid': self.transfer_id } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: return True hellraiser(res)
@login_required
[docs] def rename_file(self, fmfile, newname): """Rename file in transfer. :param fmfile: file data from filemail containing fileid :param newname: new file name :type fmfile: ``dict`` :type newname: ``str`` or ``unicode`` :rtype: ``bool`` """ if not isinstance(fmfile, dict): raise FMBaseError('fmfile must be a <dict>') method, url = get_URL('file_rename') payload = { 'apikey': self.config.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'fileid': fmfile.get('fileid'), 'filename': newname } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: self._complete = True return True hellraiser(res)
@login_required
[docs] def delete_file(self, fmfile): """Delete file from transfer. :param fmfile: file data from filemail containing fileid :type fmfile: ``dict`` :rtype: ``bool`` """ if not isinstance(fmfile, dict): raise FMFileError('fmfile must be a <dict>') method, url = get_URL('file_delete') payload = { 'apikey': self.config.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'fileid': fmfile.get('fileid') } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: self._complete = True return True hellraiser(res)
@login_required
[docs] def update(self, message=None, subject=None, days=None, downloads=None, notify=None): """Update properties for a transfer. :param message: updated message to recipient(s) :param subject: updated subject for trasfer :param days: updated amount of days transfer is available :param downloads: update amount of downloads allowed for transfer :param notify: update whether to notifiy on downloads or not :type message: ``str`` or ``unicode`` :type subject: ``str`` or ``unicode`` :type days: ``int`` :type downloads: ``int`` :type notify: ``bool`` :rtype: ``bool`` """ method, url = get_URL('update') payload = { 'apikey': self.config.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'transferid': self.transfer_id, } data = { 'message': message or self.transfer_info.get('message'), 'message': subject or self.transfer_info.get('subject'), 'days': days or self.transfer_info.get('days'), 'downloads': downloads or self.transfer_info.get('downloads'), 'notify': notify or self.transfer_info.get('notify') } payload.update(data) res = getattr(self.session, method)(url, params=payload) if res.status_code: self.transfer_info.update(data) return True hellraiser(res)
[docs] def download(self, files=None, destination=None, overwrite=False, callback=None): """Download file or files. :param files: file or files to download :param destination: destination path (defaults to users home directory) :param overwrite: replace existing files? :param callback: callback function that will receive total file size and written bytes as arguments :type files: ``list`` of ``dict`` with file data from filemail :type destination: ``str`` or ``unicode`` :type overwrite: ``bool`` :type callback: ``func`` """ if files is None: files = self.files elif not isinstance(files, list): files = [files] if destination is None: destination = os.path.expanduser('~') for f in files: if not isinstance(f, dict): raise FMBaseError('File must be a <dict> with file data') self._download(f, destination, overwrite, callback)
def _download(self, fmfile, destination, overwrite, callback): """The actual downloader streaming content from Filemail. :param fmfile: to download :param destination: destination path :param overwrite: replace existing files? :param callback: callback function that will receive total file size and written bytes as arguments :type fmfile: ``dict`` :type destination: ``str`` or ``unicode`` :type overwrite: ``bool`` :type callback: ``func`` """ fullpath = os.path.join(destination, fmfile.get('filename')) path, filename = os.path.split(fullpath) if os.path.exists(fullpath): msg = 'Skipping existing file: {filename}' logger.info(msg.format(filename=filename)) return filesize = fmfile.get('filesize') if not os.path.exists(path): os.makedirs(path) url = fmfile.get('downloadurl') stream = self.session.get(url, stream=True) def pg_callback(bytes_written): if pm.COMMANDLINE: bar.show(bytes_written) elif callback is not None: callback(filesize, bytes_written) if pm.COMMANDLINE: label = fmfile['filename'] + ': ' bar = ProgressBar(label=label, expected_size=filesize) bytes_written = 0 with open(fullpath, 'wb') as f: for chunk in stream.iter_content(chunk_size=1024 * 1024): if not chunk: break f.write(chunk) bytes_written += len(chunk) # Callback pg_callback(bytes_written) @login_required
[docs] def compress(self): """Compress files on the server side after transfer complete and make zip available for download. :rtype: ``bool`` """ method, url = get_URL('compress') payload = { 'apikey': self.config.get('apikey'), 'logintoken': self.session.cookies.get('logintoken'), 'transferid': self.transfer_id } res = getattr(self.session, method)(url, params=payload) if res.status_code == 200: return True hellraiser(res)
def __getitem__(self, key): return self.transfer_info[key] def __setitem__(self, key, value): self.transfer_info[key] = value def __repr__(self): return repr(self.transfer_info)