<file_sep># -*- coding: utf-8 -*-

# Folder.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

class Folder(object):
    def __init__(self, childCount=0):
        self.childCount = childCount<file_sep>from .Drive import Drive
from .Item import Item<file_sep>#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# 1drive.py -- cli frontend to onedrive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

import sys, os, logging
from py1drive.common import arg_is_dir
from py1drive.CLI import CLI

"""OneDrive Python client"""

config = []
default_config_dir = os.path.expanduser('~/.py1drive')
supported_actions = ['authorize',
    'upload', 'download',
    'list', 'info',
    'mkdir', 'rm']
config_file = None
key_store_dir = None

def build_arg_parser():
    import argparse
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('-c',
        '--config',
        type=argparse.FileType('r'),
        help='Config file to load')
    parser.add_argument('-l',
        '--log-level',
        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
        help='Logging verbosity',
        default='WARNING')
    parser.add_argument('-k',
        '--key-store-dir',
        type=lambda x: arg_is_dir(parser, x),
        help='Location of the authorization key store dir')
    parser.add_argument('action',
        choices=supported_actions,
        help='Action to execute on path')
    parser.add_argument('local_path',
        type=str,
        nargs='?',
        help='Path to local file/folder')
    parser.add_argument('remote_path',
        type=str,
        help='Path to remote folder')
    return parser

def exec_action(action, local_path, remote_path):
    from urllib.parse import urlparse
    url = urlparse(remote_path)
    if url.scheme == 'onedrive':
        from py1drive.OneDrive.OneDriveClient import OneDriveClient as API
    else:
        sys.exit("Unsupported scheme: %s" % url.scheme)
    if (url.scheme not in config):
        config[url.scheme] = dict()
        save_config()
    cli = CLI(API(config[url.scheme], key_store_dir=key_store_dir))
    method = getattr(cli, 'cmd_' + action, None)
    if method:
        method(local_path=local_path, remote_path=url.path)
    else:
        sys.exit("Action '%s' is not supported" % action)

def save_config():
    open(config_file, 'w').write(yaml.dump(config))

if __name__ == '__main__':
    import yaml
    parser = build_arg_parser()
    args = parser.parse_args()
    logging.basicConfig(level=getattr(logging, args.log_level))

    if (not os.path.exists(default_config_dir)):
        os.makedirs(default_config_dir)

    if (args.action == 'upload'):
        if (args.local_path is None):
            parser.error('"upload" action requires local_path')
        elif (not os.path.exists(args.local_path)):
            parser.error("invalid local_path %s, the location doesn't exist" % args.local_path)

    if (args.config is None):
        config_file = default_config_dir + '/py1drive'
    else:
        config_file = args.config.name

    if (args.key_store_dir is None):
        key_store_dir = default_config_dir
    else:
        key_store_dir = args.key_store_dir

    if (os.path.exists(config_file)):
        config = yaml.load(open(config_file, 'r'))
    else:
        config = dict()
    exec_action(args.action, args.local_path, args.remote_path)
<file_sep># -*- coding: utf-8 -*-

# Hashes.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

class Hashes(object):
    def __init__(self, **kwargs):
        if 'crc32Hash' in kwargs:
            self.crc32 = kwargs['crc32Hash']
        if 'sha1Hash' in kwargs:
            self.sha1 = kwargs['sha1Hash']<file_sep># -*- coding: utf-8 -*-

# OneDriveClient.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

import os, sys, json, yaml
from pprint import pprint
import py1drive.common as common
from py1drive.OneDrive import resources
from urllib.parse import quote


class OneDriveClient(object):
    config = []
    key_store = None
    session = dict()
    api_url = 'https://api.onedrive.com/v1.0'
    auth_base_url = 'https://login.live.com/oauth20_authorize.srf'
    auth_token_url = 'https://login.live.com/oauth20_token.srf'
    auth_scope = ['wl.offline_access', 'onedrive.readwrite']
    client = None
    is_authorized = False

    def __init__(self, config, key_store_dir):
        from requests_oauthlib import OAuth2Session

        self.config = config
        self.key_store = key_store_dir + '/oauth2_session_onedrive'
        if (os.path.exists(self.key_store)):
            self.session = yaml.load(open(self.key_store, 'r'))
        if (self.session == None):
            self.session = dict()

        self.is_authorized = ('oauth2_token' in self.session)
        self.client = OAuth2Session(self.config['client_id'],
            scope=self.auth_scope,
            redirect_uri='',
            token=self.session['oauth2_token'],
            auto_refresh_kwargs={
                'client_id': self.config['client_id'],
                'client_secret': self.config['client_secret']
            },
            auto_refresh_url=self.auth_token_url,
            token_updater=self._token_saver)

    def authorize(self, **kwargs):
        from urllib.parse import urlparse, parse_qs
        auth_url, state = self.client.authorization_url(self.auth_base_url)
        print("To allow access to your data, please open this URL in a web browser: %s" % auth_url)
        redirect_response = input('Paste the URL of the empty page: ')
        self.session['oauth2_state'] = state;
        token = self.client.fetch_token(
            self.auth_token_url,
            authorization_response=redirect_response,
            client_secret=self.config['client_secret'])
        self._token_saver(token)

    def get_drive_info(self):
        drive = resources.Drive(**self._get('/drive').json())
        return drive

    def get_item(self, remote_path):
        r = resources.Item(**self._get("/drive/root:%s" % quote(remote_path)).json())
        return r

    def get_item_children(self, remote_path=None, id=None, **kwargs):
        if remote_path:
            r = self._get("/drive/root:%s:/children" % quote(remote_path)).json()
        elif id:
            r = self._get("/drive/items/%s/children" % id).json()
        items = list()
        for value in r['value']:
            items.append(resources.Item(**value))
        return items

    def get_item_content(self, id, **kwargs):
        r = self._get("/drive/items/%s/content" % id, stream=True)
        return r

    def create_dir(self, parent_path, dir_name):
        r = self._post("/drive/root:%s:/children" % quote(parent_path),
                       data_json={"name": dir_name, "folder": {}})
        return resources.Item(**r.json())

    def _token_saver(self, token):
        self.session['oauth2_token'] = token;
        open(self.key_store, 'w').write(yaml.dump(self.session))

    def _make_url(self, method):
        return "%s%s" % (self.api_url, method)

    def _get(self, method, stream=False):
        r = self.client.get(self._make_url(method), stream=stream)
        r.raise_for_status()
        return r

    def _post(self, method, data_json=None, stream=False):
        headers = None
        data = None
        if data_json:
            data = json.dumps(data_json)
            headers = {'Content-Type' : 'application/json'}
        r = self.client.post(self._make_url(method), data=data, stream=stream, headers=headers)
        r.raise_for_status()
        return r
<file_sep># -*- coding: utf-8 -*-

# Identity.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

class Identity(object):
    def __init__(self, id=None, displayName=None):
        self.id = id
        self.displayName = displayName
<file_sep># -*- coding: utf-8 -*-

# ItemReference.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

from urllib.parse import unquote

class ItemReference(object):
    def __init__(self, id, driveId, path):
        self.id = id
        self.driveId = driveId
        self.path = unquote(path)
<file_sep># -*- coding: utf-8 -*-

# File.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

from ..types import Hashes


class File(object):
    def __init__(self, **kwargs):
        self.mimeType = kwargs['mimeType']
        self.hashes = Hashes(**kwargs['hashes'])<file_sep>from .Quota import Quota
from .Folder import Folder
from .File import File<file_sep># -*- coding: utf-8 -*-

# CLI.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

import py1drive.common as common
from py1drive.common import ProgressBar, absolute_to_relative
import os, hashlib, sys


class CLI(object):
    def __init__(self, api):
        self.API = api
        if not self.API.is_authorized:
            self.API.authorize()

    def cmd_info(self, **kwargs):
        drive = self.API.get_drive_info()
        print('Owner: ' + drive.owner.user.displayName)
        print('Used Space: ' + common.readable_size(drive.quota.used))
        print('Free Space: ' + common.readable_size(drive.quota.remaining))
        print('Total Space: ' + common.readable_size(drive.quota.total))
        print('Recycle Bin Space: ' + common.readable_size(drive.quota.deleted))
        print('State: ' + drive.quota.state)

    def cmd_list(self, remote_path, **kwargs):
        item_info = self.API.get_item(remote_path)
        items = list()
        if hasattr(item_info, 'folder'):
            item_children = self.API.get_item_children(id=item_info.id)
            items = item_children
        else:
            items.append(item_info)
        print("total %s" % len(items))
        for item in items:
            print("%12s  %s" % (common.readable_size(item.size), item.name))

    def cmd_download(self, local_path, remote_path):
        item_info = self.API.get_item(remote_path)
        if local_path == None:
            local_path = os.getcwd()

        if not os.path.exists(local_path):
            os.mkdir(local_path)

        items = self.create_download_queue(item_info)

        print('Downloading to: ' + local_path)
        print('Downloading from: ' + remote_path)
        for item in items:
            relative_path = absolute_to_relative(local_path, remote_path,
                                                item.parentReference.path + '/' + item.name)
            if hasattr(item, 'file'):
                path, file = os.path.split(relative_path)
                print("Downloading %s to %s" % (file, path))
                if not os.path.exists(path):
                    os.makedirs(path)
            self.download_item(item, relative_path)

    def cmd_upload(self, local_path, remote_path):
        pass

    def create_download_queue(self, item_info):
        items = list()
        if hasattr(item_info, 'folder'):
            children = self.API.get_item_children(id=item_info.id)
            for item in children:
                items.extend(self.create_download_queue(item))
        else:
            items.append(item_info)
        return items

    def download_item(self, item, destination_dir):
        if hasattr(item, 'folder'):
            os.mkdir(destination_dir)
        else:
            if hasattr(item.file.hashes, 'sha1'):
                item_hash = hashlib.sha1()
                item_remote_hash = item.file.hashes.sha1.lower()
            r = self.API.get_item_content(item.id)
            total_size = r.headers.get('content-length')
            bar = ProgressBar(0, total_size)
            bar.start()
            with open(destination_dir, 'wb') as out:
                for chunk in r.iter_content(chunk_size=10240):
                    if chunk:
                        bar.cur_val += len(chunk)
                        out.write(chunk)
                        item_hash.update(chunk)
                    bar.display()
                out.flush()
            print('')
            if item_hash.hexdigest().lower() != item_remote_hash:
                raise Exception('Checksum mismatch!')

    def cmd_mkdir(self, remote_path, **kwargs):
        while remote_path.endswith('/'):
            remote_path = remote_path[:len(remote_path) - 1]
        parent_path, dir_name = os.path.split(remote_path)
        if not dir_name:
            sys.exit('Please provide a path e.g.: onedrive:///path/to/folder')
        else:
            self.API.create_dir(parent_path, dir_name)
<file_sep># -*- coding: utf-8 -*-

# Quota.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

class Quota(object):
    def __init__(self, total=0, used=0, remaining=0, deleted=0, state=None):
        self.total = total
        self.used = used
        self.remaining = remaining
        self.deleted = deleted
        self.state = state
<file_sep>from .Identity import Identity
from .IdentitySet import IdentitySet
from .ItemReference import ItemReference
from .Hashes import Hashes<file_sep># -*- coding: utf-8 -*-

# Drive.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

from ..types import IdentitySet
from ..facets import Quota


class Drive(object):
    def __init__(self, id=None, driveType=None, owner=None, quota=None, **kwargs):
        self.id = id
        self.driveType = driveType
        self.owner = IdentitySet(**owner)
        self.quota = Quota(**quota)
<file_sep># -*- coding: utf-8 -*-

# common.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

import os, math, time, sys

def readable_size(size):
    if size == 0:
        return '0 B '
    size_units = ('B', 'KB', 'MB', 'GB', 'TB')
    unit = min(math.floor(math.log(size, 1024)), len(size_units) - 1)
    size_fmt = round(size / math.pow(1024, unit), 2)
    return "%s %s" % (size_fmt, size_units[unit])

def arg_is_dir(parser, path):
    if (os.path.isdir(path)):
        return path
    else:
        parser.error('%s is not a valid folder' % path)

def absolute_to_relative(local, remote, path):
    return local + path[path.index(remote) + len(remote):]

class ProgressBar(object):
    def __init__(self, min_val, max_val):
        self.min_val = int(min_val)
        if not max_val is None:
            self.max_val = int(max_val)
        self.cur_val = int(min_val)

    def start(self):
        self.start_time = time.clock()

    def display(self):
        speed = readable_size(self.cur_val//(time.clock() - self.start_time))
        if self.max_val is None:
            sys.stdout.write("\r[%s] %s %s/s" % ('=' * 50, readable_size(self.cur_val), speed))
        else:
            progress = 50 * self.cur_val / self.max_val
            int_progress = int(progress)
            sys.stdout.write("\r%3.1f%% [%s%s] %s of %s %s/s" %
                             ((progress/50*100), '=' * int_progress, ' ' * (50 - int_progress),
                              readable_size(self.cur_val), readable_size(self.max_val), speed))
<file_sep># -*- coding: utf-8 -*-

# Item.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

from ..types import ItemReference, IdentitySet
from ..facets import Folder, File
from urllib.parse import unquote

class Item(object):
    def __init__(self, **kwargs):
        self.id = kwargs['id']
        self.name = unquote(kwargs['name'])
        if 'parentReference' in kwargs:
            self.parentReference = ItemReference(**kwargs['parentReference'])
        self.createdBy = IdentitySet(**kwargs['createdBy'])
        self.createdDateTime = kwargs['createdDateTime']
        self.lastModifiedDateTime = kwargs['lastModifiedDateTime']
        self.size = kwargs['size']
        if 'children' in kwargs:
            self.children = list()
            for child in kwargs['children']:
                self.children.append(Item(**child))
        if 'folder' in kwargs:
            self.folder = Folder(**kwargs['folder'])
        if 'file' in kwargs:
            self.file = File(**kwargs['file'])<file_sep># -*- coding: utf-8 -*-

# IdentitySet.py -- part of py1drive
# Copyright (C) 2015 <NAME>

# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.

from .Identity import Identity

class IdentitySet(object):
    def __init__(self, **kwargs):
        if 'user' in kwargs:
            self.user = Identity(**kwargs['user'])
        if 'application' in kwargs:
            self.application = Identity(**kwargs['application'])
        if 'device' in kwargs:
            self.device = Identity(**kwargs['device'])
