nk9 / get_dropbox_link

Code to get the URL of a file in the Dropbox folder
MIT License
6 stars 1 forks source link

Refresh token now keeps expiring, need a way to auto regenerate #1

Closed rfiorentino1 closed 1 year ago

rfiorentino1 commented 1 year ago

Hi, Thank you so much for writing this, I'm using Dropbox with a Synology Nas, and needed a way to re-create the native dropbox app functionality when browsing the synology's directories. I'm using this in conjunction with service station/AppleScript to create an option in the context menu to copy a link from the current folder which matches dropbox. Took me forever to find a working python script, and this is the one! Really appreciate it.

However, I have discovered that the refresh token expires from dropbox every four hours... I'm really not that familiar with python, so I'm not sure how to get the script to automatically request a new refresh token, though it seems to be fairly easy from what I've read online thus far. I tried to ask chat GPT for help as well, but no luck lol :-) I also tried replacing it with an access token, but that didn't work either. Open to any suggestions/thoughts. Thanks again

nk9 commented 1 year ago

Can you confirm that the Python code is running locally? I'm a bit unclear how the NAS fits into the picture.

I ask because using the OAuth2 flow (used to refresh the access token) requires you to log into the Dropbox website using a browser. If the code isn't running locally, I'm not really clear how to launch the web session in a way that will allow you to enter your credentials. (It might involve creating some sort of Synology plugin.)

rfiorentino1 commented 1 year ago

Hi Nick, thanks so much for getting back to me so quickly. The code is definitely running locally on macOS; I basically just need the python script to know when it sees an expired token, and regenerate a new one silently in the background so that I don't have to manually generate it again every time I use the script after four hours have passed. The synology is really just an SMB share to the Mac, and the Apple script in between is converting from the synology's path to the local Dropbox. I can post the applescript later today for context, but basically it's converting /Volumes/Dropbox/path (the SMB share) to ~/Dropbox/path (local Dropbox folder) and passing that path through the python script so it can run.

rfiorentino1 commented 1 year ago

So far, I've tried adding both an access token and a refresh token as variables, and then:

with dropbox.Dropbox(oauth2_access_token=ACCESS_TOKEN, oauth2_refresh_token=REFRESH_TOKEN) as dbx:
    try:
        dbx.users_get_current_account()
    except dropbox.exceptions.AuthError:
        sys.exit(
            "ERROR: Invalid access token; try re-generating an "
            "access token from the app console on the web."
        )

But this hasn't worked, likely because I'm not familiar enough with Python and have an error, or because I'm not understanding something in the Dropbox auth flow. :)

nk9 commented 1 year ago

This is the simplest and most complete explanation of the necessary steps I can find.

The key thing is that the refresh token needs to be stored after the auth flow returns it and then supplied to instantiate the dbx object. There's a lot going on in the code supplied by the last commentator in the original thread, but this is how he's doing it.

I don't have much time to devote to this right now, and I'm not actually using Dropbox ATM. But this is indeed the right way to do it and I'd be happy to review/accept a PR if you can get this working!

rfiorentino1 commented 1 year ago

Ok, cool, thank you for that.

This is where I've gotten to: I seem to have figured out the authentication part, with this script:

#!/usr/bin/env python3

import os
import dropbox
from dropbox import DropboxOAuth2FlowNoRedirect

#Populate your app key in order to run this locally
APP_KEY = ""

auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, use_pkce=True, token_access_type='offline')

# Check if the refresh token file exists and contains a token
if os.path.exists('refresh_token.txt'):
    with open('refresh_token.txt', 'r') as f:
        refresh_token = f.read().strip()
        if refresh_token:
            print('Found refresh token in file')
            dbx = dropbox.Dropbox(oauth2_refresh_token=refresh_token, app_key=APP_KEY)
            dbx.users_get_current_account()
            print("Successfully set up client!")
            exit(0)

# If the refresh token file doesn't exist or doesn't contain a token, go through the authorization flow process
authorize_url = auth_flow.start()
print("1. Go to: " + authorize_url)
print("2. Click \"Allow\" (you might have to log in first).")
print("3. Copy the authorization code.")
auth_code = input("Enter the authorization code here: ").strip()

try:
    oauth_result = auth_flow.finish(auth_code)
except Exception as e:
    print('Error: %s' % (e,))
    exit(1)

# View the details of the oauth result
print(f'Access Token  = {oauth_result.access_token}')
print(f'Account ID    = {oauth_result.account_id}')
print(f'Refresh Token = {oauth_result.refresh_token}')

def saved_token(refresh_token):
    with open('refresh_token.txt', 'w') as f:
        f.write(refresh_token)
    return refresh_token.decode()

# Store the refresh token to use over and over whenever an access token expires
refresh_token = saved_token(oauth_result.refresh_token)

# Set up the Dropbox client
dbx = dropbox.Dropbox(oauth2_refresh_token=refresh_token, app_key=APP_KEY)

# Test the connection
dbx.users_get_current_account()
print("Successfully set up client!")

And then I attempted to merge it into your existing script, which I can't figure out. Here's my awful attempt ;)

#!/Users/roccofiorentino/.pyenv/shims/python3

# coding=utf-8

# Copyright 2021 Nick Kocharhook
# MIT Licensed

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

import dropbox
from dropbox.exceptions import ApiError

import argparse
from pathlib import Path as P
import sys
import json
import logging

import os
from dropbox import DropboxOAuth2FlowNoRedirect

# Populate your app key in order to run this locally
APP_KEY = ""

auth_flow = DropboxOAuth2FlowNoRedirect(APP_KEY, use_pkce=True, token_access_type='offline')

# Check if the refresh token file exists and contains a token
if os.path.exists('refresh_token.txt'):
    with open('refresh_token.txt', 'r') as f:
        refresh_token = f.read().strip()
        if refresh_token:
            print('Found refresh token in file')
            dbx = dropbox.Dropbox(oauth2_refresh_token=refresh_token, app_key=APP_KEY)
            dbx.users_get_current_account()
            print("Successfully set up client!")
            exit(0)

# If the refresh token file doesn't exist or doesn't contain a token, go through the authorization flow process
authorize_url = auth_flow.start()
print("1. Go to: " + authorize_url)
print("2. Click \"Allow\" (you might have to log in first).")
print("3. Copy the authorization code.")
auth_code = input("Enter the authorization code here: ").strip()

try:
    oauth_result = auth_flow.finish(auth_code)
except Exception as e:
    print('Error: %s' % (e,))
    exit(1)

# View the details of the oauth result
print(f'Access Token  = {oauth_result.access_token}')
print(f'Account ID    = {oauth_result.account_id}')
print(f'Refresh Token = {oauth_result.refresh_token}')

# Store the refresh token to use over and over whenever an access token expires
with open('refresh_token.txt', 'w') as f:
    f.write(oauth_result.refresh_token)

# Set up the Dropbox client
dbx = dropbox.Dropbox(oauth2_refresh_token=oauth_result.refresh_token, app_key=APP_KEY)

# Test the connection
dbx.users_get_current_account()
print("Successfully set up client!")

# Either 'personal' or 'business'. Must match the account which generated
# the TOKEN above.
# See <https://help.dropbox.com/installs-integrations/desktop/locate-dropbox-folder>
ACCOUNT_TYPE = "business"

def main():
    local_dbx_path = None
    args = parseArguments()

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)

        try:
            with open(P.home() / ".dropbox/info.json") as jsonf:
                info = json.load(jsonf)
                local_dbx_path = info[ACCOUNT_TYPE]["path"]
        except Exception:
            logging.error("Couldn't find Dropbox folder path")
            sys.exit(1)

        for path in args.paths:
            try:
                p = P(path).absolute()
                logging.debug(f"Processing file at path {p}")
                relp = p.relative_to(local_dbx_path)
                dbx_path = f"/{relp}"
            except Exception as e:
                logging.error(str(e))
                sys.exit(1)

            try:
                link = dbx.sharing_create_shared_link(dbx_path)
                print(link.url)
            except ApiError as e:
                logging.error(str(e))
                sys.exit(1)

def parseArguments():
    parser = argparse.ArgumentParser(description="Fetch Dropbox URL for path")
    parser.add_argument("paths", type=str, nargs="+", help="paths to files")
    parser.add_argument(
        "--verbose", "-v", action="store_true", help="toggle verbose mode"
    )

    args = parser.parse_args()

    return args

if __name__ == "__main__":
    main()
nk9 commented 1 year ago

Thanks for that! I have adapted your code into a new commit (a2759b6a83bde029f91badd53d4e3adc62572d46) and updated the README. The script now uses a refresh token. I'm glad future users will get the benefit of this!

rfiorentino1 commented 1 year ago

Awesome, thank you so much! I really appreciate it

nk9 commented 1 year ago

Glad to help! FYI, I've made some further changes to cache the access token too, as suggested in the original thread. I've also removed an unnecessary API call. This should speed up generating links in most cases by ~2–3x.