cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 
Announcements
Want to know what we learned at IBC? Check out our learnings on media, remote working and more right here.

View, download, and export

Need support with viewing, downloading, and exporting files and folders from your Dropbox account? Find help from the Dropbox Community.

cancel
Showing results for 
Show  only  | Search instead for 
Did you mean: 

Key Command/Shortcut to "Copy Dropbox Link" from Mac Finder.

Key Command/Shortcut to "Copy Dropbox Link" from Mac Finder.

Justin P.2
Collaborator | Level 10
Go to solution

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 instead of right clicking.

67 Replies 67

markcq
Helpful | Level 5
Go to solution

This doesn't really sense to me... I downloaded the script... no idea what to do with it... I tried to do the whole automation thing, but also don't know what to do with that... so just got dead-end and dead-end and dead-end... Surely by now DropBox would have done something around this... I use Copy Dropbox link anywhere from 10–100 times per day... honestly getting tired of right clicking (on a track pad) 10–100 times per day 😓

Gene_Parmesan
Collaborator | Level 8
Go to solution

@markcq wrote:

This doesn't really sense to me... I downloaded the script... no idea what to do with it... I tried to do the whole automation thing, but also don't know what to do with that... so just got dead-end and dead-end and dead-end... Surely by now DropBox would have done something around this... I use Copy Dropbox link anywhere from 10–100 times per day... honestly getting tired of right clicking (on a track pad) 10–100 times per day 😓


Sorry it's not working for you. Happy to try to answer specific questions, but there isn't enough information here to say what's wrong. I've expanded on the instructions a bit in a new repository, have a look and see if that helps. Definitely agree that Dropbox should allow this feature out of the box, it's really annoying that we have to set up workarounds like this.

Gene_Parmesan
Collaborator | Level 8
Go to solution

@Здравко 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.

Здравко
Legendary | Level 20
Go to solution

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_Parmesan
Collaborator | Level 8
Go to solution

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.

DavidW2
Helpful | Level 5
Go to solution
Are there updated instructions on how to set it up?

Здравко
Legendary | Level 20
Go to solution

Hi @DavidW2,

If you follow the install steps in @Gene_Parmesan's answer, but use my script (see above), doesn't it works? 🤔 If so, can you post details about what error are you getting? Again, it's unlikely the scripts, posted here (not only the my one), work on team/business account - some more coding has to be done, but not added yet (in all posted scripts), by now! For private/individual account (of any subscription type), it should work though. What is your subscription? 🧐

Gene_Parmesan
Collaborator | Level 8
Go to solution

Yes, @DavidW2, it's just a minor change to App Key from generating an App Token. You can read full instructions in the README. I don't have a business account, but a previous poster said that they were able to get my script working by changing the ACCOUNT_TYPE variable, set as part of the README instructions. If there's more to it, unfortunately I'm not able to help with that.

 

I haven't tried running your script, @Здравко, although I did read it to puzzle out how your caching worked. That, along with this forum thread and some ideas on config files from this post went into the new design.

Здравко
Legendary | Level 20
Go to solution

@Gene_Parmesan wrote:

...

I haven't tried running your script, @Здравко, although I did read it to puzzle out how your caching worked. ...


To be honest, I'm not sure can understand what you ask actually. 🤔 It's as trivial as possible. I just keep the authentication data ("refresh token", "access token", and access token' expiration time), both on Initial OAuth flow and on every refresh latter. On script start, when config is available, it's used for client object initialization, otherwise asks user for authorization. 🤷 That's it - nothing more.

By the way your script will fail if pointed file has a link already! You don't check/handle such situation. Do it. 😉

Hope this sheds some light.

Gene_Parmesan
Collaborator | Level 8
Go to solution

I have repeatedly made links to the same file with no errors, so I haven't run into this issue. If you do experience a problem, you're welcome to open an issue in the repo with reproduction steps.

Need more support?
Who's talking

Top contributors to this post

  • User avatar
    Здравко Legendary | Level 20
  • User avatar
    wilcoxaj New member | Level 2
  • User avatar
    Gene_Parmesan Collaborator | Level 8
What do Dropbox user levels mean?