You might see that the Dropbox Community team have been busy working on some major updates to the Community itself! So, here is some info on what’s changed, what’s staying the same and what you can expect from the Dropbox Community overall.
Forum Discussion
Justin P.2
9 years agoCollaborator | Level 10
Key Command/Shortcut to "Copy Dropbox Link" from Mac Finder.
I copy download links all day from the Mac Finder. It would be great to assign a key command so that you can highlight a file in your Dropbox via Mac Finder, and press a key command to copy the link ...
- 4 years ago
Update (2023-01-17)
I've moved the script to a repository and expanded on the instructions.
I wanted this now, so I wrote a Python script to get the URL from the command line and created an Automator QuickAction that calls the script with the selected file(s) in Finder:
Now, ^⌘L copies the Dropbox URL of any number of files selected in the Dropbox folder.
Gene_Parmesan
Collaborator | Level 8
Здравко wrote:Hi Gene_Parmesan,
Migrate your code to use refresh token (together with the access one). Otherwise your script users will need access token update by hands quite often. Also cache the access token and wipe out all calls not related to the link receiving to speed up the process (every call - meaningful or not - takes time).
Thanks for the suggetions. I've moved the code into a repo and would be hppy to accept a pull request.
Здравко
2 years agoLegendary | Level 20
Hi Gene_Parmesan,
I just made some illustrative implementation just to show what I mean. It can be used in the same way you used your script and instead of continuously changing access token, register your application and the access token will becomes updated automatically (without your intervention). The only change needed is set correct application key (your application key) once - at the beginning. Don't forget to register correct redirect URL in your application.
#!/usr/bin/python3
###############################################################################
# Script receiving Dropbox links based on referred local files/folders
# --------------------------------------------------------------------
# Every target file/folder is passed as argument to the script. Resulted links
# are passed in the same sequence one per line to standard output (incorrect
# are skipped).
# Two preparation steps are needed:
# 1. Register a Dropbox application and put the application key as value of
# APPLICATION_KEY global variable.
# 2. Register the used REDIRECT_URI in the application just created in
# previous step. With host 'localhost' and port 8080, the redirect URL
# that has to be registered is "http://localhost:8080/", for instance.
# Next, just make it executable (if needed), using:
# $ chmod a+x get_dropbox_link
# ... and put it in a place visible for execution in your system (somewhere in
# folders pointed by $PATH environment variable). On first run you will be
# invited to link your script to your Dropbox account. To work correct local
# Dropbox application and this script have to be link to the same account!
# Author: Здравко
# www.dropboxforum.com/t5/user/viewprofilepage/user-id/422790
###############################################################################
from dropbox import Dropbox, DropboxOAuth2Flow
from dropbox.exceptions import ApiError
from dropbox.oauth import NotApprovedException
import json
from pathlib import Path
from datetime import datetime
from os import sep
from sys import exit
import logging
# Place to save current configuration
CONFIG_JSON='~/.getlink_conf.json'
# Take a look on your application in https://www.dropbox.com/developers/apps
APPLICATION_KEY='PUT YOUR KEY HERE'
URI_HOST='localhost'
URI_PORT=8080
# URI should be registered in the application redirect URIs list!!!
REDIRECT_URI=f"http://{URI_HOST}:{URI_PORT}/"
HOST_PORT=(URI_HOST,URI_PORT)
success_response = "End of authentication flow. Your can get a link!"
cancel_response = "🤷 You have denied your application's work. "
error_response = " You got an error: "
class ApplicationConfig:
def __init__(self, conf_path=CONFIG_JSON):
self.conf_path=Path(conf_path).expanduser()
self.conf=None
self.client=None
self.access_token=None
self.access_token_expiresat=None
self.refresh_token=None
if self.conf_path.is_file():
try:
with self.conf_path.open() as fconf:
self.conf=json.load(fconf)
self.access_token = self.conf['access_token']
self.access_token_expiresat = datetime.fromtimestamp(
self.conf['access_token_expiresat'])
self.refresh_token = self.conf['refresh_token']
except Exception:
self.conf_path.unlink(True)
self.conf=None
def __del__(self):
"Checks for something changed (new access token) and dumps it when there is"
if (self.client is not None and
self.client._oauth2_access_token_expiration >
self.access_token_expiresat):
self.conf['access_token'] = self.client._oauth2_access_token
self.conf['access_token_expiresat'] = (
self.client._oauth2_access_token_expiration.timestamp())
self.conf['refresh_token'] = self.client._oauth2_refresh_token
with self.conf_path.open(mode='w') as fconf:
json.dump(self.conf, fconf)
def getClient(self):
"Gets Dropbox client object. Performs OAuth flow if needed."
if self.conf is None:
self.client=None
import webbrowser
from http.server import HTTPServer, BaseHTTPRequestHandler
dbxAuth=DropboxOAuth2Flow(APPLICATION_KEY, REDIRECT_URI, {},
'dropbox-auth-csrf-token', token_access_type='offline', use_pkce=True)
webbrowser.open(dbxAuth.start())
conf=None
conf_path = self.conf_path
class Handler(BaseHTTPRequestHandler):
response_success = success_response.encode()
response_cancel = cancel_response.encode()
response_error = error_response.encode()
def do_GET(self):
nonlocal dbxAuth, conf
from urllib.parse import urlparse, parse_qs
query = parse_qs(urlparse(self.path).query)
for r in query.keys():
query[r] = query[r][0]
self.send_response(200)
self.send_header("content-type", "text/plain;charset=UTF-8")
try:
oauthRes = dbxAuth.finish(query)
conf={'access_token': oauthRes.access_token,
'access_token_expiresat': oauthRes.expires_at.timestamp(),
'refresh_token': oauthRes.refresh_token}
with conf_path.open(mode='w') as fconf:
json.dump(conf, fconf)
except NotApprovedException:
conf={}
self.send_header("content-length", f"{len(Handler.response_cancel)}")
self.end_headers()
self.wfile.write(Handler.response_cancel)
self.wfile.flush()
return
except Exception as e:
conf={}
r = Handler.response_error + str(e).encode()
self.send_header("content-length", f"{len(r)}")
self.end_headers()
self.wfile.write(r)
self.wfile.flush()
return
self.send_header("content-length", f"{len(Handler.response_success)}")
self.end_headers()
self.wfile.write(Handler.response_success)
self.wfile.flush()
httpd=HTTPServer((URI_HOST, URI_PORT), Handler)
while conf is None:
httpd.handle_request()
httpd.server_close()
del httpd
if 'refresh_token' not in conf:
raise RuntimeError("Cannot process because missing authentication")
self.conf = conf
self.access_token = self.conf['access_token']
self.access_token_expiresat = datetime.fromtimestamp(
self.conf['access_token_expiresat'])
self.refresh_token = self.conf['refresh_token']
# Makes sure there is cached client object.
if self.client is None:
self.client=Dropbox(self.access_token,
oauth2_refresh_token=self.refresh_token,
oauth2_access_token_expiration=self.access_token_expiresat,
app_key=APPLICATION_KEY)
return self.client
class PathMapper:
def __init__(self):
dbx_info = Path('~/.dropbox/info.json').expanduser()
if not dbx_info.is_file():
raise RuntimeError("Missing Dropbox application information")
with dbx_info.open() as finfo:
# Only personal accounts are supported by now - group accounts need
# additional namespace handling (just changing 'personal' is not enough).
# Somebody else may make some exercises.
self.dbx_path = json.load(finfo)['personal']['path']
def __contains__(self, path):
path = str(Path(path).expanduser().absolute())
return ((len(path) == len(self.dbx_path) and path == self.dbx_path) or
(len(path) > len(self.dbx_path) and path[len(self.dbx_path)] == sep
and path[:len(self.dbx_path)] == self.dbx_path))
def __getitem__(self, path):
path = str(Path(path).expanduser().absolute())
if ((len(path) == len(self.dbx_path) and path == self.dbx_path) or
(len(path) > len(self.dbx_path) and path[len(self.dbx_path)] == sep
and path[:len(self.dbx_path)] == self.dbx_path)):
return path[len(self.dbx_path):]
def main():
import argparse
dbxPathMap = PathMapper()
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()
del parser
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
conf = ApplicationConfig()
dbx = conf.getClient()
for path in args.paths:
if path not in dbxPathMap:
logging.error(f"Passed path '{path}' is not part of the Dropbox driectory tree")
continue
dbx_path = dbxPathMap[path]
if len(dbx_path) == 0:
logging.error("Dropbox folder itself cannot be pointed by link")
continue
logging.debug(f"Processing local file '{path}' with Dropbox path '{dbx_path}'")
try:
metadata = dbx.sharing_create_shared_link_with_settings(dbx_path)
except ApiError as e:
er = e.error
if not er.is_shared_link_already_exists():
raise
er = er.get_shared_link_already_exists()
if not er.is_metadata():
raise
metadata = er.get_metadata()
print(metadata.url)
if __name__ == "__main__":
try:
main()
except Exception as e:
logging.error(f"Unexpected error: {e}")
exit(1)
There are some imperfections mentioned in comments that you can improve. 😉 One thing that can be improved too is plain text response replace to HTML (better views in web browser).
Good luck.
- Gene_Parmesan2 years agoCollaborator | Level 8
Thanks for giving the updated script a try, dhermanq, sorry it’s not working for you. 🤔 Porg is having a similar issue, so clearly the instructions could use some refinement.
There are several suggestions in this Stack Overflow issue. Because I’m not able to reproduce the issue, I fear it will be up to you to do the actual troubleshooting, but do report back if you find something that works.
The first thing I’d try would be to install Dropbox this way:python3 -m pip install dropbox
- Gene_Parmesan2 years agoCollaborator | Level 8
I've spent a bit more time with this and I think I've resolved the Python and Automator issues. Please see the new instructions in the repo README, and let me know if there are any further problems! dhermanq porg Note that I have fixed a problem with duplicate links caused by an unneeded "Get Selected Finder Items" action.
I've added threading too. Now even relatively large sets of links can be generated efficiently. There are 10 workers by default, hopefully that will strike a good balance between speed and keeping on the right side of any rate limits on Dropbox's API.
- dhermanq2 years agoHelpful | Level 6
Gene_Parmesan thanks for the additional help on this! I've made some progress with the new script and instructions. I took your suggestion to pin urllib3 to version 1.26.6, and that allowed me to generate the API access code. However, now when I try running my Automator workflow I get the following error:
The action “Run Shell Script” encountered an error: “usage: get_dropbox_link.py [-h] [--verbose] paths [paths ...]
get_dropbox_link.py: error: the following arguments are required: paths” in macro “Copy Dropbox Link”Would this have something to do with the fact that either a) I use a Dropbox business account (I've changed the AccountType variable in your script accordingly), or b) I keep my Dropbox files on a removable drive, and not the standard ~/Dropbox path?
I do appreciate for all the thought and attention you've put into this thus far!
- Gene_Parmesan2 years agoCollaborator | Level 8
Ack, that's my error! It should be "$@" like you had it, I'm sorry. Please change it back.
Just to be clear, when you're getting this error, are you pressing Run within Automator? If so, that won't work. You need to do the next steps to set up the Finder keyboard shortcut. That way the Workflow will be run as a Quick Action via the Services menu and thus have selected files in Finder to pass into the Quick Action.
- dhermanq2 years agoHelpful | Level 6
Gene_ParmesanSuccess! Previously I had been using a Keyboard Maestro macro to run the Automator workflow, but I've just tried triggering it from the services menu with a keyboard shortcut, and voila! It works!
I'm curious how the two methods differ. I had used KM to run your previous version of the script without any trouble. In any case, this method works great and the link seems to be generated much much quicker than before! A massive improvement -- thank you again for this!!
- Gene_Parmesan2 years agoCollaborator | Level 8
Huzzah! So glad we got there in the end, thanks for helping me debug the script, LOL. And I'm really pleased to hear it's faster now!
My guess is that the difference comes from this version of the workflow requiring Finder items to be passed in at the top. Previously, I was using a Get Selected Finder Items action, which was resulting in links being duplicated given the "Workflow receives files from Finder" preamble. But if you add back that Get Selected Finder Items action and then go back to using KM (and I presume set the workflow to receive "no input", since that's how you'd now be using it)… I think it would work that way as well. You can read up on this a bit on AskDifferent.
I'd be interested to hear if my hypothesis is correct. 😀 I had assumed these two approaches were completely equivalent, but perhaps using an action is actually more flexible.
- dhermanq2 years agoHelpful | Level 6
That was it! When I restored "Get selected item from Finder" and changed the input to none, my Keyboard Maestro macro worked. But after a quick and dirty test, it looks like it's just a hair faster to run the workflow as a service, so I'm going to keep it that way after all.
Thanks Gene_Parmesan !!
- Gene_Parmesan2 years agoCollaborator | Level 8
Great, glad we could lay that mystery to rest. Yeah, I'm not surprised that it's faster via the Services menu. As far as I understand it, having that extra Workflow step means another AppleScript invocation, and AppleScript is notoriously sluggish.
- dhermanq2 years agoHelpful | Level 6
Gene_Parmesanthe update did the trick! Thank you!!
- Gene_Parmesan2 years agoCollaborator | Level 8
With the help of a user on GitHub, I have changed the script as suggested to use a refresh token and also to save and reuse the access token. I also found an unnecessary API call which was running each time and just slowing things down. So dhermanq, please give this new version a try and see if it speeds things up for you.
I'd suggest everyone upgrade, since this no longer requires generating OAuth2 tokens on the website every few hours. It's a pretty big quality of life improvement.
About View, download, and export
Need support with viewing, downloading, and exporting files and folders from your Dropbox account? Find help from the Dropbox Community.
Need more support
If you need more help you can view your support options (expected response time for an email or ticket is 24 hours), or contact us on X or Facebook.
For more info on available support options for your Dropbox plan, see this article.
If you found the answer to your question in this Community thread, please 'like' the post to say thanks and to let us know it was useful!