0

我在网上获得了下面的代码,我正在尝试指定 2 个文件夹进行同步:我手机上的整个 Pythonista 目录和我的 Dropbox 中的单个“Pythonista”文件夹。我可能可以在我的小屏幕上通过大量滚动来解决这个问题,但对于不是 python 专家的人来说,我认为这里的某个人可以更快地解决这个问题:

# See: 
# http://www.devwithimagination.com/2014/05/11/pythonista-dropbox-sync
# http://www.devwithimagination.com/2016/06/14/pythonista-dropbox-sync-revisited/ 

import webbrowser, os
import dropbox
import hashlib
import json
import difflib
import sys
import logging
import re
import console
from functools import partial

# Python 3 compatibility
try: 
    input = raw_input
except NameError:
    pass

# Program, do not edit from here

# custom logging level
FINE = 15

# file locations used by the program
PYTHONISTA_DOC_DIR = os.path.expanduser('~/Documents')
SYNC_FOLDER_NAME = 'dropbox_sync'
SYNC_STATE_FOLDER = os.path.join(PYTHONISTA_DOC_DIR, SYNC_FOLDER_NAME)
SYNC_STATE_FILENAME = 'file.cache.txt'
CONFIG_FILENAME = 'PythonistaDropbox.conf'
CONFIG_FILEPATH = os.path.join(SYNC_STATE_FOLDER, CONFIG_FILENAME)

# default file extensions which will be processed
DEFAULT_FILE_EXTENSIONS = ['.py', '.pyui', '.txt', '.conf']

# default list of files that shouldn't be synced
DEFAULT_SKIP_FILES = [os.path.join(SYNC_FOLDER_NAME, SYNC_STATE_FILENAME)]

# dict holding options the user has chosen to remember
REMEMBER_OPTIONS = {}

# Method to get the MD5 Hash of the file with the supplied file name.
def getHash(file_name):
    # Open the file to read in binary format
    with open(file_name, mode='rb') as f:
        d = hashlib.md5()
        # Read the file a bit at a time to conserve memory
        for buf in iter(partial(f.read, 128), b''):
            d.update(buf)
    return d.hexdigest()

# Helper method to determine if a local file is eligible for sync
def can_sync_local_file(config, file):

    relative_path = os.path.relpath(file, PYTHONISTA_DOC_DIR)
    file_name = os.path.basename(file)

    if not relative_path in config['skip_files'] and not file_name.startswith('.') and not os.path.isdir(file):

        file_ext = os.path.splitext(file)[1]

        if file_ext in (config['file_extensions']) or [m.group(0) for l in config['file_extensions'] for m in [re.match('[\.]?\*',l)] if m]:
            return True

    return False

# Method to determine if the supplied local folder contains any files which would be eligible for sync
def can_sync_local_directory(config, local_folder):

    dir_name = os.path.basename(local_folder)

    if os.path.exists(local_folder) and not os.path.relpath(local_folder, PYTHONISTA_DOC_DIR) in config['skip_files'] and not dir_name.startswith('.'):
        files = os.listdir(local_folder)
        for current_file in files:

            full_path = os.path.join(local_folder, current_file)
            relative_path = os.path.relpath(full_path, PYTHONISTA_DOC_DIR)
            db_path = '/'+relative_path

            if can_sync_local_file(config, full_path):
                return True

            elif os.path.isdir(full_path):

                    files_found = can_sync_local_directory(config, full_path)

                    if files_found:
                        # Something in the directory needs to be synced
                        return True


    logging.debug('Directory %s does not contain any files for sync', local_folder)
    return False

# Write the updated configuration
def write_configuration(config):
    with open(CONFIG_FILEPATH, 'w') as config_file:
            json.dump(config, config_file, indent=1)

# Generates an authorized Dropbox client object.
# This will use cached OAUTH credentials if they have been stored, otherwise the
# user will be put through the Dropbox authentication process.
def get_dropbox_client(configuration):
    if not 'access_token' in configuration:
        setup_new_auth_token(configuration)
    return dropbox.client.DropboxClient(configuration["access_token"])

# Method to set up a new Dropbox OAUTH2 access token.
# This will take the user through the required steps to authenticate.
def setup_new_auth_token(configuration):
    flow = dropbox.client.DropboxOAuth2FlowNoRedirect(configuration["APP_KEY"], configuration["APP_SECRET"])
    url = flow.start()

    # Make the user sign in and authorize this token
    logging.debug('url: %s', url)
    logging.info('1. Visit this website and press the "Allow" button.')
    logging.info('2. Copy the authorization code.')
    webbrowser.open(url)
    code = input("3. Paste the authorization code here and hit [enter]: ")
    # This will fail if the user didn't visit the above URL and hit 'Allow'
    access_token, user_id = flow.finish(code)
    # update configuration with token
    print(access_token)
    configuration['access_token'] = access_token

    write_configuration(configuration)

def upload(file, details, client, parent_revision):
    logging.log(FINE, 'Trying to upload %s', file)
    details['md5hash'] = getHash(file)
    logging.log(FINE, 'New MD5 hash: %s', details['md5hash'])

    with open(os.path.join(PYTHONISTA_DOC_DIR, file), 'rb') as in_file:
        response = client.put_file(file, in_file, False, parent_revision)

    logging.debug('Response: %s', response)
    details = update_file_details(details, response)

    logging.info('Uploaded %s', file)

    return details

def download(dest_path, dropbox_metadata, details, client):
    with open(os.path.join(PYTHONISTA_DOC_DIR, dest_path), 'wb') as out_file:
        f = client.get_file(dropbox_metadata['path'])
        out_file.write(f.read())

    details['md5hash'] = getHash(dest_path)
    logging.log(FINE, 'New MD5 hash: %s', details['md5hash'])
    logging.info('Downloaded %s', dest_path)
    return update_file_details(details, dropbox_metadata)

def process_folder(config, client, dropbox_dir, file_details):

    # Get the metadata for the directory being processed (dropbox_dir).
    # If the directory does not exist on Dropbox it will be created.
    try:
        folder_metadata = client.metadata(dropbox_dir)

        logging.debug('metadata: %s', folder_metadata)

        if 'is_deleted' in folder_metadata:
            # directory is deleted, create
            client.file_create_folder(dropbox_dir)
            folder_metadata = client.metadata(dropbox_dir)          

    except dropbox.rest.ErrorResponse as error:
        logging.debug(error.status)
        if error.status == 404:
            client.file_create_folder(dropbox_dir)
            folder_metadata = client.metadata(dropbox_dir)
        else:
            logging.exception(error)
            raise error

    # If the directory does not exist locally, create it.
    local_folder = os.path.join(PYTHONISTA_DOC_DIR, dropbox_dir[1:])
    if not os.path.exists(local_folder):
        os.mkdir(local_folder)


    # All the files that have been processed so far in this folder.
    processed_files = []
    # All the directories that exist on Dropbox in the current folder that need to be processed.
    dropbox_dirs = ['Pythonista']
    # All the local directories in this current folder that do not exist in Dropbox.
    local_dirs = ['~/Documents']

    # Go through the files currently in Dropbox and compare with local
    for file in folder_metadata['contents']:
        dropbox_path = file['path'][1:]
        file_name = file['path'].split('/')[-1]

        file_ext = os.path.splitext(file_name)[1]

        if file['is_dir'] == False and (file_ext in config['file_extensions'] or [m.group(0) for l in config['file_extensions'] for m in [re.match('[\.]?\*',l)] if m]):

            if not os.path.exists(os.path.join(PYTHONISTA_DOC_DIR, dropbox_path)):
                logging.info('Processing Dropbox file %s (%s)', file['path'], dropbox_path)

                try:


                    if dropbox_path in file_details:
                        # in cache but file no longer locally exists
                        details = file_details[dropbox_path]

                        if 'SYNC_NO_LOCAL' in REMEMBER_OPTIONS:
                            prev_choice = REMEMBER_OPTIONS['SYNC_NO_LOCAL']
                        else:
                            prev_choice = ''

                        if prev_choice in ('la', 'da', 'sa'):
                            choice = prev_choice[0]
                        else:

                            choice = input('''File %s is in the sync cache and on Dropbox, but no longer exists locally. (Default Delete):
Delete From Dropbox (d) [All in this state (da)]
Download File (l) [All in this state (la)]
Skip (s) [All in this state (sa)]
''' % file['path']).lower()

                        # remember options if necessary
                        if choice in ('la', 'da', 'sa'):
                            REMEMBER_OPTIONS['SYNC_NO_LOCAL'] = choice
                            choice = choice[0]

                        if (choice == 'l'):
                            download_file = True
                        elif (choice == 'd' or not choice):
                            # Default is 'del'
                            download_file = False

                            #delete the dropbox copy
                            client.file_delete(file['path'])
                            file_details.remove(dropbox_path)

                    else:
                        details = {}
                        download_file = True

                    if download_file:
                        logging.info('Downloading file %s (%s)', file['path'], dropbox_path)
                        logging.debug(details)

                        details = download(dropbox_path, file, details, client)
                        file_details[dropbox_path] = details

                    # dealt with this file, don't want to touch it again later
                    processed_files.append(file_name)
                    write_sync_state(file_details)

                except:
                    pass
            else:
                # need to check if we should update this file
                # is this file in our map?
                if dropbox_path in file_details:
                    details = file_details[dropbox_path]

                    logging.debug('Held details are: %s', details)

                    if details['revision'] == file['revision']:
                        # same revision
                        current_hash = getHash(dropbox_path)

                        logging.debug('New hash: %s, Old hash: %s', current_hash, details['md5hash'])

                        if current_hash == details['md5hash']:
                            logging.log(FINE, 'File "%s" not changed.', dropbox_path)
                        else:
                            logging.log(FINE, 'File "%s" updated locally, uploading...', dropbox_path)

                            details = upload(dropbox_path, details, client, file['rev'])
                            file_details[dropbox_path] = details

                        processed_files.append(file_name)
                    else:
                        #different revision
                        logging.log(FINE, 'Revision of "%s" changed from %s to %s. ', dropbox_path, details['revision'], file['revision'])

                        current_hash = getHash(dropbox_path)

                        logging.debug('File %s. New hash: %s, Old hash: %s', dropbox_path, current_hash, details['md5hash'])

                        if current_hash == details['md5hash']:
                            logging.log(FINE, 'File "%s" updated remotely. Downloading...', dropbox_path)

                            details = download(dropbox_path, file, details, client)
                            file_details[dropbox_path] = details
                        else:

                            if 'UPDATED_BOTH' in REMEMBER_OPTIONS:
                                prev_choice = REMEMBER_OPTIONS['UPDATED_BOTH']
                            else:
                                prev_choice = ''

                            if prev_choice in ('la', 'da', 'sa'):
                                choice = prev_choice[0]
                            else:
                                choice = input('''File %s has been updated both locally and on Dropbox. (Default Skip) Overwrite: 
Dropbox Copy (d) [All in this state (da)]
Local Copy (l) [All in this state (la)]
Skip (s) [All in this state (sa)]
''' % file['path']).lower()

                            # remember options if necessary
                            if choice in ('la', 'da', 'sa'):
                                REMEMBER_OPTIONS['UPDATED_BOTH'] = choice
                                choice = choice[0]

                            if choice == 'd':
                                logging.log(FINE, 'Overwriting Dropbox Copy of %s', file)
                                details = upload(dropbox_path, details, client, file['rev'])
                                file_details[dropbox_path] = details
                            elif choice == 'l':
                                logging.log(FINE, 'Overwriting Local Copy of %s', file)
                                details = download(dropbox_path, file, details, client)
                                file_details[dropbox_path] = details


                else:
                    # Not in cache, but exists on dropbox and local, need to prompt user
                    if 'NO_SYNC_BOTH' in REMEMBER_OPTIONS:
                        prev_choice = REMEMBER_OPTIONS['NO_SYNC_BOTH']
                    else:
                        prev_choice = ''

                    if prev_choice in ('la', 'da', 'sa'):
                        choice = prev_choice[0]
                    else:
                        choice = input('''File %s is not in the sync cache but exists both locally and on dropbox. (Default Skip) Overwrite:
Dropbox Copy (d) [All in this state (da)]
Local Copy (l) [All in this state (la)]
Skip (s) [All in this state (sa)]
 ''' % file['path']).lower()

                    # remember options if necessary
                    if choice in ('la', 'da', 'sa'):
                        REMEMBER_OPTIONS['NO_SYNC_BOTH'] = choice
                        choice = choice[0]

                    details = {}
                    if choice == 'd':
                        logging.log(FINE, 'Overwriting Dropbox Copy of %s', file)
                        details = upload(dropbox_path, details, client, file['rev'])
                        file_details[dropbox_path] = details
                    elif choice == 'l':
                        logging.log(FINE, 'Overwriting Local Copy of %s', file)
                        details = download(dropbox_path, file, details, client)
                        file_details[dropbox_path] = details
                    else:
                        logging.log(FINE, 'Skipping processing for file %s', file)

                # Finished dealing with this file, update the sync state and mark this file as processed.
                write_sync_state(file_details)
                processed_files.append(file_name)
        # edit
        #elif file['is_dir'] and 'is_deleted' not in file:
            #dropbox_dirs.append(file['path'])


    # go through the files that are local but not on Dropbox, upload these.
    files = os.listdir(local_folder)
    for file in files:

        full_path = os.path.join(local_folder, file)
        relative_path = os.path.relpath(full_path, PYTHONISTA_DOC_DIR)
        db_path = '/'+relative_path

        if not file in processed_files and not relative_path in config['skip_files'] and not os.path.isdir(full_path) and not file.startswith('.'):

            filename, file_ext = os.path.splitext(file)

            if file_ext in (config['file_extensions']) or [m.group(0) for l in config['file_extensions'] for m in [re.match('[\.]?\*',l)] if m]:


                logging.debug('Searching "%s" for "%s"', dropbox_dir, file)
                # this search includes dropbox_dir AND CHILD DIRS!
                search_results = client.search(dropbox_dir, file)

                logging.debug(search_results)

                found = False
                for single_result in search_results:
                    if single_result['path'] == db_path:
                        found = True

                if found:
                    logging.warning("File found on Dropbox, this shouldn't happen! Skipping %s...", file)
                else:
                    logging.debug(relative_path)

                    upload_file = False

                    # check if an upload or a local delete is required
                    if relative_path in file_details:
                        # File is not in dropbox but is in sync cache
                        details = file_details[relative_path]

                        if 'SYNC_NO_DROP' in REMEMBER_OPTIONS:
                            prev_choice = REMEMBER_OPTIONS['SYNC_NO_DROP']
                        else:
                            prev_choice = ''

                        if prev_choice in ('da', 'ua', 'sa'):
                            choice = prev_choice[0]
                        else:
                            choice = input('''File %s is in the sync cache but no longer on Dropbox. (Default Delete):
Delete local file (d) [All in this state (da)]
Upload File (u) [All in this state (ua)
Skip (s) [All in this state (sa)]
''' % relative_path).lower()

                        # remember options if necessary
                        if choice in ('ua', 'da', 'sa'):
                            REMEMBER_OPTIONS['SYNC_NO_DROP'] = choice
                            choice = choice[0]

                        if choice == 'u':
                            upload_file = True
                        elif (choice == 'd' or not choice):
                            # delete file
                            os.remove(full_path)

                            # update sync state
                            del file_details[relative_path]
                            write_sync_state(file_details)
                    else:
                        details = {}
                        upload_file = True

                    logging.debug('Details were %s', details)

                    # upload the file
                    if upload_file:
                        details = upload(relative_path, details, client, None )
                        file_details[relative_path] = details
                        write_sync_state(file_details)

            else:
                logging.debug("Skipping extension %s", file_ext)

        #elif not db_path in dropbox_dirs and os.path.isdir(full_path) and can_sync_local_directory(config, full_path):
            #local_dirs.append(db_path)


    #process the directories
    for folder in dropbox_dirs:
        logging.debug('Processing dropbox dir %s from %s', folder, dropbox_dir)
        if folder[1:] not in config['skip_files']:
            process_folder(config, client, folder, file_details)
        else:
            logging.log(FINE, 'Skipping dropbox directory %s', folder)

    for folder in local_dirs:
        logging.debug('Processing local dir %s from %s', folder, dropbox_dir)
        if folder[1:] not in config['skip_files']:
            process_folder(config, client, folder, file_details)
        else:
            logging.log(FINE, 'Skipping local directory %s', folder)

    # delete the folder if empty
    folder_metadata = client.metadata(dropbox_dir)
    if len(folder_metadata['contents']) == 0 and 'is_deleted' not in folder_metadata:
        # empty remote directory - delete
        logging.info('Remote directory %s is empty, deleting...', dropbox_dir)
        logging.debug('Pre-delete metadata %s', folder_metadata)
        client.file_delete(dropbox_dir)



def update_file_details(file_details, dropbox_metadata):
    for key in 'revision rev modified path'.split():
        file_details[key] = dropbox_metadata[key]
    return file_details

def write_sync_state(file_details):
    # Write sync state file
    sync_status_file = os.path.join(SYNC_STATE_FOLDER, SYNC_STATE_FILENAME)

    logging.debug('Writing sync state to %s', sync_status_file)

    with open(sync_status_file, 'w') as output_file:
        json.dump(file_details, output_file)

# prompt user for additional (optional) configuration options
def setup_user_configuration(prompt, configuration):

    if prompt:

        configuration['file_extensions'] = input('''What file extensions should be synced? New extensions must be prefixed with a dot, and be comma separated. (These will be included by default %s)
''' % DEFAULT_FILE_EXTENSIONS).replace(', ',',').split(',')

        logging.debug(input)

        configuration['skip_files'] = input('''What files should not be synced? Paths should be relative to the root and be comma separated.
''').replace(', ',',').split(',')

        write_configuration(configuration)

    # add missing options if not user configured
    if 'file_extensions' not in configuration:
        configuration['file_extensions'] = []

    for ext in DEFAULT_FILE_EXTENSIONS:
        if ext not in configuration['file_extensions']:
            configuration['file_extensions'].append(ext)

    logging.log(FINE, 'File extensions: %s', configuration['file_extensions'])

    if 'skip_files' not in configuration:
        configuration['skip_files'] = []

    for file in DEFAULT_SKIP_FILES:
        if file not in configuration['skip_files']:
            configuration['skip_files'].append(file)

    logging.log(FINE, 'Skip files: %s', configuration['skip_files'])

# Load the configuration file, if it exists. 
# if a configuration file does not exist this will prompt
# the user for inital configuration values      
def setup_configuration():

    if not os.path.exists(SYNC_STATE_FOLDER):
        os.mkdir(SYNC_STATE_FOLDER)
    if os.path.exists(CONFIG_FILEPATH):
        with open(CONFIG_FILEPATH, 'r') as config_file:
            config = json.load(config_file)
    else:
        logging.log(FINE, 'Configuration file missing')
        config = {}

        logging.info('Get your app key and secret from the Dropbox developer website')

        config['APP_KEY'] = input('''Enter your app key
''')
        config['APP_SECRET'] = input('''Enter your app secret
''')

        # ACCESS_TYPE can be 'dropbox' or 'app_folder' as configured for your app
        config['ACCESS_TYPE'] = 'app_folder'


        # Write the config file back
        write_configuration(config)

    return config



# Load the current sync status file, if it exists, and return the contents.
# if the file does not exist an empty object will be returned. 
def load_sync_state():

    sync_status_file = os.path.join(SYNC_STATE_FOLDER, SYNC_STATE_FILENAME)

    if not os.path.exists(SYNC_STATE_FOLDER):
        os.mkdir(SYNC_STATE_FOLDER)
    if os.path.exists(sync_status_file):
        with open(sync_status_file, 'r') as input_file:
            try:
                file_details = json.load(input_file)
            except ValueError:
                # Corrupted sync status file
                # (May have crashed during syncing)
                # Discard and start over
                logging.warning('Error parsing sync status file! Proceeding manually.')
                file_details = {}
    else:
        file_details = {}

    logging.debug('File Details: %s', file_details)

    return file_details


def main():

    # Process any supplied arguments
    log_level = 'INFO'
    update_config = False

    for argument in sys.argv:
        if argument.lower() == '-v':
            log_level = 'FINE'
        elif argument.lower() == '-vv':
            log_level = 'DEBUG'
        elif argument.lower() == '-c':
            update_config = True

    # configure logging
    log_format = "%(message)s"

    logging.addLevelName(FINE, 'FINE')
    for handler in logging.getLogger().handlers:
        logging.getLogger().removeHandler(handler)
    logging.basicConfig(format=log_format, level=log_level)


    # disable dimming the screen
    console.set_idle_timer_disabled(True)

    # Load the current sync status file
    file_details = load_sync_state()

    # Load the initial configuration
    config = setup_configuration()

    # set up user configuration options
    setup_user_configuration(update_config, config)

    logging.info('Begin Dropbox sync')

    #configure dropbox
    client = get_dropbox_client(config)

    logging.info('linked account: %s', client.account_info()['display_name'])
    os.chdir(PYTHONISTA_DOC_DIR) # Switch to Pythonista doc root if not there already
    process_folder(config, client, '/', file_details)

    # Write sync state file
    write_sync_state(file_details)

    # re-enable dimming the screen
    console.set_idle_timer_disabled(False)


if __name__ == "__main__":
    main()
    logging.info('Dropbox sync done!')

我尝试了以下方法:

dropbox_dirs = ['Pythonista']
local_dirs = ['~/Documents']

上面的内容以前是空列表,带有稍后评论的附加内容。但这仍然会导致文件被存放在最上面的保管箱目录中,并且并非所有来自 Pythonista 的文件夹/文件都被同步。我刚刚意识到代码会查找某些扩展名的文件,有没有办法同步完整的文件夹?

***如果在 py 文件的顶部以某种方式指定了同步文件夹,那就太好了。

感谢任何/所有帮助,在此先感谢!此外,如果存在我不知道的更好的 DropBox 同步脚本,它也会起作用!

4

1 回答 1

0

这会在最外层的 Pythonista 目录中正确导入一个文件:

# http://www.devwithimagination.com/2014/05/11/pythonista-dropbox-sync
# http://www.devwithimagination.com/2016/06/14/pythonista-dropbox-sync-revisited/ 

import webbrowser, os
import dropbox
import hashlib
import json
import difflib
import sys
import logging
import re
import console
from functools import partial

# Python 3 compatibility
try: 
    input = raw_input
except NameError:
    pass

# Program, do not edit from here

# custom logging level
FINE = 15

# file locations used by the program
PYTHONISTA_DOC_DIR = os.path.expanduser('~/Documents')
DROPBOX_SYNC_FOLDER = '/Pythonista'
SYNC_FOLDER_NAME = 'dropbox_sync'
SYNC_STATE_FOLDER = os.path.join(PYTHONISTA_DOC_DIR, SYNC_FOLDER_NAME)
SYNC_STATE_FILENAME = 'file.cache.txt'
CONFIG_FILENAME = 'PythonistaDropbox.conf'
CONFIG_FILEPATH = os.path.join(SYNC_STATE_FOLDER, CONFIG_FILENAME)

# default file extensions which will be processed
DEFAULT_FILE_EXTENSIONS = ['.py', '.pyui', '.txt', '.conf']

# default list of files that shouldn't be synced
DEFAULT_SKIP_FILES = [os.path.join(SYNC_FOLDER_NAME, SYNC_STATE_FILENAME)]

# dict holding options the user has chosen to remember
REMEMBER_OPTIONS = {}

# Method to get the MD5 Hash of the file with the supplied file name.
def getHash(file_name):
    # Open the file to read in binary format
    with open(file_name, mode='rb') as f:
        d = hashlib.md5()
        # Read the file a bit at a time to conserve memory
        for buf in iter(partial(f.read, 128), b''):
            d.update(buf)
    return d.hexdigest()

# Helper method to determine if a local file is eligible for sync
def can_sync_local_file(config, file):

    relative_path = os.path.relpath(file, PYTHONISTA_DOC_DIR)
    file_name = os.path.basename(file)

    if not relative_path in config['skip_files'] and not file_name.startswith('.') and not os.path.isdir(file):

        file_ext = os.path.splitext(file)[1]

        if file_ext in (config['file_extensions']) or [m.group(0) for l in config['file_extensions'] for m in [re.match('[\.]?\*',l)] if m]:
            return True

    return False

# Method to determine if the supplied local folder contains any files which would be eligible for sync
def can_sync_local_directory(config, local_folder):

    dir_name = os.path.basename(local_folder)

    if os.path.exists(local_folder) and not os.path.relpath(local_folder, PYTHONISTA_DOC_DIR) in config['skip_files'] and not dir_name.startswith('.'):
        files = os.listdir(local_folder)
        for current_file in files:

            full_path = os.path.join(local_folder, current_file)
            relative_path = os.path.relpath(full_path, PYTHONISTA_DOC_DIR)
            db_path = '/'+relative_path

            if can_sync_local_file(config, full_path):
                return True

            elif os.path.isdir(full_path):

                    files_found = can_sync_local_directory(config, full_path)

                    if files_found:
                        # Something in the directory needs to be synced
                        return True


    logging.debug('Directory %s does not contain any files for sync', local_folder)
    return False

# Write the updated configuration
def write_configuration(config):
    with open(CONFIG_FILEPATH, 'w') as config_file:
            json.dump(config, config_file, indent=1)

# Generates an authorized Dropbox client object.
# This will use cached OAUTH credentials if they have been stored, otherwise the
# user will be put through the Dropbox authentication process.
def get_dropbox_client(configuration):
    if not 'access_token' in configuration:
        setup_new_auth_token(configuration)
    return dropbox.client.DropboxClient(configuration["access_token"])

# Method to set up a new Dropbox OAUTH2 access token.
# This will take the user through the required steps to authenticate.
def setup_new_auth_token(configuration):
    flow = dropbox.client.DropboxOAuth2FlowNoRedirect(configuration["APP_KEY"], configuration["APP_SECRET"])
    url = flow.start()

    # Make the user sign in and authorize this token
    logging.debug('url: %s', url)
    logging.info('1. Visit this website and press the "Allow" button.')
    logging.info('2. Copy the authorization code.')
    webbrowser.open(url)
    code = input("3. Paste the authorization code here and hit [enter]: ")
    # This will fail if the user didn't visit the above URL and hit 'Allow'
    access_token, user_id = flow.finish(code)
    # update configuration with token
    print(access_token)
    configuration['access_token'] = access_token

    write_configuration(configuration)

def upload(file, details, client, parent_revision):
    logging.log(FINE, 'Trying to upload %s', file)
    details['md5hash'] = getHash(file)
    logging.log(FINE, 'New MD5 hash: %s', details['md5hash'])

    with open(os.path.join(PYTHONISTA_DOC_DIR, file), 'rb') as in_file:
        # edit
        print(file)
        response = client.put_file(os.path.join(DROPBOX_SYNC_FOLDER, file), in_file, False, parent_revision)

    logging.debug('Response: %s', response)
    details = update_file_details(details, response)

    logging.info('Uploaded %s', file)

    return details

def download(dest_path, dropbox_metadata, details, client):
    with open(os.path.join(PYTHONISTA_DOC_DIR, dest_path), 'wb') as out_file:
        f = client.get_file(dropbox_metadata['path'])
        out_file.write(f.read())

    details['md5hash'] = getHash(dest_path)
    logging.log(FINE, 'New MD5 hash: %s', details['md5hash'])
    logging.info('Downloaded %s', dest_path)
    return update_file_details(details, dropbox_metadata)

def process_folder(config, client, dropbox_dir, file_details):

    # Get the metadata for the directory being processed (dropbox_dir).
    # If the directory does not exist on Dropbox it will be created.
    try:
        folder_metadata = client.metadata(dropbox_dir)

        logging.debug('metadata: %s', folder_metadata)

        if 'is_deleted' in folder_metadata:
            # directory is deleted, create
            client.file_create_folder(dropbox_dir)
            folder_metadata = client.metadata(dropbox_dir)          

    except dropbox.rest.ErrorResponse as error:
        logging.debug(error.status)
        if error.status == 404:
            client.file_create_folder(dropbox_dir)
            folder_metadata = client.metadata(dropbox_dir)
        else:
            logging.exception(error)
            raise error

    # If the directory does not exist locally, create it.
    local_folder = os.path.join(PYTHONISTA_DOC_DIR, dropbox_dir[1:])
    if not os.path.exists(local_folder):
        os.mkdir(local_folder)


    # All the files that have been processed so far in this folder.
    processed_files = []
    # All the directories that exist on Dropbox in the current folder that need to be processed.
    dropbox_dirs = ['Pythonista']
    # All the local directories in this current folder that do not exist in Dropbox.
    local_dirs = []

    # Go through the files currently in Dropbox and compare with local
    for file in folder_metadata['contents']:
        dropbox_path = file['path'][1:]
        file_name = file['path'].split('/')[-1]

        file_ext = os.path.splitext(file_name)[1]

        if file['is_dir'] == False and (file_ext in config['file_extensions'] or [m.group(0) for l in config['file_extensions'] for m in [re.match('[\.]?\*',l)] if m]):

            if not os.path.exists(os.path.join(PYTHONISTA_DOC_DIR, dropbox_path)):
                logging.info('Processing Dropbox file %s (%s)', file['path'], dropbox_path)

                try:


                    if dropbox_path in file_details:
                        # in cache but file no longer locally exists
                        details = file_details[dropbox_path]

                        if 'SYNC_NO_LOCAL' in REMEMBER_OPTIONS:
                            prev_choice = REMEMBER_OPTIONS['SYNC_NO_LOCAL']
                        else:
                            prev_choice = ''

                        if prev_choice in ('la', 'da', 'sa'):
                            choice = prev_choice[0]
                        else:

                            choice = input('''File %s is in the sync cache and on Dropbox, but no longer exists locally. (Default Delete):
Delete From Dropbox (d) [All in this state (da)]
Download File (l) [All in this state (la)]
Skip (s) [All in this state (sa)]
''' % file['path']).lower()

                        # remember options if necessary
                        if choice in ('la', 'da', 'sa'):
                            REMEMBER_OPTIONS['SYNC_NO_LOCAL'] = choice
                            choice = choice[0]

                        if (choice == 'l'):
                            download_file = True
                        elif (choice == 'd' or not choice):
                            # Default is 'del'
                            download_file = False

                            #delete the dropbox copy
                            client.file_delete(file['path'])
                            file_details.remove(dropbox_path)

                    else:
                        details = {}
                        download_file = True

                    if download_file:
                        logging.info('Downloading file %s (%s)', file['path'], dropbox_path)
                        logging.debug(details)

                        details = download(dropbox_path, file, details, client)
                        file_details[dropbox_path] = details

                    # dealt with this file, don't want to touch it again later
                    processed_files.append(file_name)
                    write_sync_state(file_details)

                except:
                    pass
            else:
                # need to check if we should update this file
                # is this file in our map?
                if dropbox_path in file_details:
                    details = file_details[dropbox_path]

                    logging.debug('Held details are: %s', details)

                    if details['revision'] == file['revision']:
                        # same revision
                        current_hash = getHash(dropbox_path)

                        logging.debug('New hash: %s, Old hash: %s', current_hash, details['md5hash'])

                        if current_hash == details['md5hash']:
                            logging.log(FINE, 'File "%s" not changed.', dropbox_path)
                        else:
                            logging.log(FINE, 'File "%s" updated locally, uploading...', dropbox_path)

                            details = upload(dropbox_path, details, client, file['rev'])
                            file_details[dropbox_path] = details

                        processed_files.append(file_name)
                    else:
                        #different revision
                        logging.log(FINE, 'Revision of "%s" changed from %s to %s. ', dropbox_path, details['revision'], file['revision'])

                        current_hash = getHash(dropbox_path)

                        logging.debug('File %s. New hash: %s, Old hash: %s', dropbox_path, current_hash, details['md5hash'])

                        if current_hash == details['md5hash']:
                            logging.log(FINE, 'File "%s" updated remotely. Downloading...', dropbox_path)

                            details = download(dropbox_path, file, details, client)
                            file_details[dropbox_path] = details
                        else:

                            if 'UPDATED_BOTH' in REMEMBER_OPTIONS:
                                prev_choice = REMEMBER_OPTIONS['UPDATED_BOTH']
                            else:
                                prev_choice = ''

                            if prev_choice in ('la', 'da', 'sa'):
                                choice = prev_choice[0]
                            else:
                                choice = input('''File %s has been updated both locally and on Dropbox. (Default Skip) Overwrite: 
Dropbox Copy (d) [All in this state (da)]
Local Copy (l) [All in this state (la)]
Skip (s) [All in this state (sa)]
''' % file['path']).lower()

                            # remember options if necessary
                            if choice in ('la', 'da', 'sa'):
                                REMEMBER_OPTIONS['UPDATED_BOTH'] = choice
                                choice = choice[0]

                            if choice == 'd':
                                logging.log(FINE, 'Overwriting Dropbox Copy of %s', file)
                                details = upload(dropbox_path, details, client, file['rev'])
                                file_details[dropbox_path] = details
                            elif choice == 'l':
                                logging.log(FINE, 'Overwriting Local Copy of %s', file)
                                details = download(dropbox_path, file, details, client)
                                file_details[dropbox_path] = details


                else:
                    # Not in cache, but exists on dropbox and local, need to prompt user
                    if 'NO_SYNC_BOTH' in REMEMBER_OPTIONS:
                        prev_choice = REMEMBER_OPTIONS['NO_SYNC_BOTH']
                    else:
                        prev_choice = ''

                    if prev_choice in ('la', 'da', 'sa'):
                        choice = prev_choice[0]
                    else:
                        choice = input('''File %s is not in the sync cache but exists both locally and on dropbox. (Default Skip) Overwrite:
Dropbox Copy (d) [All in this state (da)]
Local Copy (l) [All in this state (la)]
Skip (s) [All in this state (sa)]
 ''' % file['path']).lower()

                    # remember options if necessary
                    if choice in ('la', 'da', 'sa'):
                        REMEMBER_OPTIONS['NO_SYNC_BOTH'] = choice
                        choice = choice[0]

                    details = {}
                    if choice == 'd':
                        logging.log(FINE, 'Overwriting Dropbox Copy of %s', file)
                        details = upload(dropbox_path, details, client, file['rev'])
                        file_details[dropbox_path] = details
                    elif choice == 'l':
                        logging.log(FINE, 'Overwriting Local Copy of %s', file)
                        details = download(dropbox_path, file, details, client)
                        file_details[dropbox_path] = details
                    else:
                        logging.log(FINE, 'Skipping processing for file %s', file)

                # Finished dealing with this file, update the sync state and mark this file as processed.
                write_sync_state(file_details)
                processed_files.append(file_name)

        # edit
        # elif file['is_dir'] and 'is_deleted' not in file:
            # dropbox_dirs.append(file['path'])

    print(dropbox_dirs)

    # go through the files that are local but not on Dropbox, upload these.
    files = os.listdir(local_folder)
    for file in files:

        full_path = os.path.join(local_folder, file)
        relative_path = os.path.relpath(full_path, PYTHONISTA_DOC_DIR)
        db_path = '/'+relative_path

        if not file in processed_files and not relative_path in config['skip_files'] and not os.path.isdir(full_path) and not file.startswith('.'):

            filename, file_ext = os.path.splitext(file)

            if file_ext in (config['file_extensions']) or [m.group(0) for l in config['file_extensions'] for m in [re.match('[\.]?\*',l)] if m]:


                logging.debug('Searching "%s" for "%s"', dropbox_dir, file)
                # this search includes dropbox_dir AND CHILD DIRS!
                search_results = client.search(dropbox_dir, file)

                logging.debug(search_results)

                found = False
                for single_result in search_results:
                    if single_result['path'] == db_path:
                        found = True

                if found:
                    logging.warning("File found on Dropbox, this shouldn't happen! Skipping %s...", file)
                else:
                    logging.debug(relative_path)

                    upload_file = False

                    # check if an upload or a local delete is required
                    if relative_path in file_details:
                        # File is not in dropbox but is in sync cache
                        details = file_details[relative_path]

                        if 'SYNC_NO_DROP' in REMEMBER_OPTIONS:
                            prev_choice = REMEMBER_OPTIONS['SYNC_NO_DROP']
                        else:
                            prev_choice = ''

                        if prev_choice in ('da', 'ua', 'sa'):
                            choice = prev_choice[0]
                        else:
                            choice = input('''File %s is in the sync cache but no longer on Dropbox. (Default Delete):
Delete local file (d) [All in this state (da)]
Upload File (u) [All in this state (ua)
Skip (s) [All in this state (sa)]
''' % relative_path).lower()

                        # remember options if necessary
                        if choice in ('ua', 'da', 'sa'):
                            REMEMBER_OPTIONS['SYNC_NO_DROP'] = choice
                            choice = choice[0]

                        if choice == 'u':
                            upload_file = True
                        elif (choice == 'd' or not choice):
                            # delete file
                            os.remove(full_path)

                            # update sync state
                            del file_details[relative_path]
                            write_sync_state(file_details)
                    else:
                        details = {}
                        upload_file = True

                    logging.debug('Details were %s', details)

                    # upload the file
                    if upload_file:
                        print(relative_path)
                        # edit
                        details = upload(relative_path, details, client, None)
                        file_details[relative_path] = details
                        write_sync_state(file_details)

            else:
                logging.debug("Skipping extension %s", file_ext)

        elif not db_path in dropbox_dirs and os.path.isdir(full_path) and can_sync_local_directory(config, full_path):
            local_dirs.append(db_path)


    #process the directories
    for folder in dropbox_dirs:
        logging.debug('Processing dropbox dir %s from %s', folder, dropbox_dir)
        if folder[1:] not in config['skip_files']:
            process_folder(config, client, folder, file_details)
        else:
            logging.log(FINE, 'Skipping dropbox directory %s', folder)

    for folder in local_dirs:
        logging.debug('Processing local dir %s from %s', folder, dropbox_dir)
        if folder[1:] not in config['skip_files']:
            process_folder(config, client, folder, file_details)
        else:
            logging.log(FINE, 'Skipping local directory %s', folder)

    # delete the folder if empty
    folder_metadata = client.metadata(dropbox_dir)
    if len(folder_metadata['contents']) == 0 and 'is_deleted' not in folder_metadata:
        # empty remote directory - delete
        logging.info('Remote directory %s is empty, deleting...', dropbox_dir)
        logging.debug('Pre-delete metadata %s', folder_metadata)
        client.file_delete(dropbox_dir)



def update_file_details(file_details, dropbox_metadata):
    for key in 'revision rev modified path'.split():
        file_details[key] = dropbox_metadata[key]
    return file_details

def write_sync_state(file_details):
    # Write sync state file
    sync_status_file = os.path.join(SYNC_STATE_FOLDER, SYNC_STATE_FILENAME)

    logging.debug('Writing sync state to %s', sync_status_file)

    with open(sync_status_file, 'w') as output_file:
        json.dump(file_details, output_file)

# prompt user for additional (optional) configuration options
def setup_user_configuration(prompt, configuration):

    if prompt:

        configuration['file_extensions'] = input('''What file extensions should be synced? New extensions must be prefixed with a dot, and be comma separated. (These will be included by default %s)
''' % DEFAULT_FILE_EXTENSIONS).replace(', ',',').split(',')

        logging.debug(input)

        configuration['skip_files'] = input('''What files should not be synced? Paths should be relative to the root and be comma separated.
''').replace(', ',',').split(',')

        write_configuration(configuration)

    # add missing options if not user configured
    if 'file_extensions' not in configuration:
        configuration['file_extensions'] = []

    for ext in DEFAULT_FILE_EXTENSIONS:
        if ext not in configuration['file_extensions']:
            configuration['file_extensions'].append(ext)

    logging.log(FINE, 'File extensions: %s', configuration['file_extensions'])

    if 'skip_files' not in configuration:
        configuration['skip_files'] = []

    for file in DEFAULT_SKIP_FILES:
        if file not in configuration['skip_files']:
            configuration['skip_files'].append(file)

    logging.log(FINE, 'Skip files: %s', configuration['skip_files'])

# Load the configuration file, if it exists. 
# if a configuration file does not exist this will prompt
# the user for inital configuration values      
def setup_configuration():

    if not os.path.exists(SYNC_STATE_FOLDER):
        os.mkdir(SYNC_STATE_FOLDER)
    if os.path.exists(CONFIG_FILEPATH):
        with open(CONFIG_FILEPATH, 'r') as config_file:
            config = json.load(config_file)
    else:
        logging.log(FINE, 'Configuration file missing')
        config = {}

        logging.info('Get your app key and secret from the Dropbox developer website')

        config['APP_KEY'] = input('''Enter your app key
''')
        config['APP_SECRET'] = input('''Enter your app secret
''')

        # ACCESS_TYPE can be 'dropbox' or 'app_folder' as configured for your app
        config['ACCESS_TYPE'] = 'app_folder'


        # Write the config file back
        write_configuration(config)

    return config



# Load the current sync status file, if it exists, and return the contents.
# if the file does not exist an empty object will be returned. 
def load_sync_state():

    sync_status_file = os.path.join(SYNC_STATE_FOLDER, SYNC_STATE_FILENAME)

    if not os.path.exists(SYNC_STATE_FOLDER):
        os.mkdir(SYNC_STATE_FOLDER)
    if os.path.exists(sync_status_file):
        with open(sync_status_file, 'r') as input_file:
            try:
                file_details = json.load(input_file)
            except ValueError:
                # Corrupted sync status file
                # (May have crashed during syncing)
                # Discard and start over
                logging.warning('Error parsing sync status file! Proceeding manually.')
                file_details = {}
    else:
        file_details = {}

    logging.debug('File Details: %s', file_details)

    return file_details


def main():

    # Process any supplied arguments
    log_level = 'INFO'
    update_config = False

    for argument in sys.argv:
        if argument.lower() == '-v':
            log_level = 'FINE'
        elif argument.lower() == '-vv':
            log_level = 'DEBUG'
        elif argument.lower() == '-c':
            update_config = True

    # configure logging
    log_format = "%(message)s"

    logging.addLevelName(FINE, 'FINE')
    for handler in logging.getLogger().handlers:
        logging.getLogger().removeHandler(handler)
    logging.basicConfig(format=log_format, level=log_level)


    # disable dimming the screen
    console.set_idle_timer_disabled(True)

    # Load the current sync status file
    file_details = load_sync_state()

    # Load the initial configuration
    config = setup_configuration()

    # set up user configuration options
    setup_user_configuration(update_config, config)

    logging.info('Begin Dropbox sync')

    #configure dropbox
    client = get_dropbox_client(config)

    logging.info('linked account: %s', client.account_info()['display_name'])
    os.chdir(PYTHONISTA_DOC_DIR) # Switch to Pythonista doc root if not there already
    process_folder(config, client, '/', file_details)

    # Write sync state file
    write_sync_state(file_details)

    # re-enable dimming the screen
    console.set_idle_timer_disabled(False)


if __name__ == "__main__":
    main()
    logging.info('Dropbox sync done!')

然而,很多都没有按预期工作。当前的问题是某种同步循环。我稍后会尝试解决这个问题。现在可以看到添加了 DROPBOX_SYNC_FOLDER。

于 2017-05-20T18:46:02.453 回答