Dropbox has deprecated API v1., and so this very useful script no longer works.
Forum Archive
Dropbox file picker needs update
This is my first post. I will try to include source.
import json
def list_folder(folder_path='/'):
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Content-Type': 'application/json' }
if folder_path == '/': folder_path = ''
data = {'path':folder_path}
r = requests.post('https://api.dropboxapi.com/2/files/list_folder', headers=headers, data=json.dumps(data))
result = r.json()
return result.get('entries', None)
def download_file(path, dest_filename, progress=None):
data = {'path':path}
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Dropbox-API-Arg': json.dumps(data)}
url = 'https://content.dropboxapi.com/2/files/download'
r = requests.post(url, stream=True, headers=headers)
You also need to change 'path' to 'path_display' in two places in def load_folder.
I've been using the v2 dropbox for a year, but hadn't tried to update any pythonista code until today. I have only done a few minutes of testing.
for info in infos:
path = info.get('path_display')
name = os.path.split(path)[1]
if name.startswith('.'):
continue
is_dir = info.get('is_dir', False)
item = {'title': name, 'image': 'ionicons-folder-32' if is_dir else 'ionicons-ios7-download-outline-32', 'accessory_type': 'disclosure_indicator' if is_dir else 'none', 'is_dir': is_dir, 'path': info['path_display']}
items.append(item)
Ok. Now I see how to include source.
import requests
import urllib
import os
import ui
import json
def list_folder(folder_path='/'):
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Content-Type': 'application/json' }
if folder_path == '/': folder_path = ''
data = {'path':folder_path}
r = requests.post('https://api.dropboxapi.com/2/files/list_folder', headers=headers, data=json.dumps(data))
result = r.json()
return result.get('entries', None)
def download_file(path, dest_filename, progress=None):
data = {'path':path}
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Dropbox-API-Arg': json.dumps(data)}
url = 'https://content.dropboxapi.com/2/files/download'
r = requests.post(url, stream=True, headers=headers)
You also need to replace 'path' with 'path_dislpay' in twice in def load_folder.
@ui.in_background
def load_folder(self):
infos = list_folder(self.path)
items = []
if self.path != '/':
items.append({'title': '..', 'image': 'ionicons-arrow-up-c-32', 'up': True})
if not infos:
import console
console.alert('Error', 'Could not load folder. Please check if you entered the access token correctly.', 'OK', hide_cancel_button=True)
self.status_label.hidden = True
return
for info in infos:
path = info.get('path_display')
name = os.path.split(path)[1]
if name.startswith('.'):
continue
is_dir = info.get('is_dir', False)
item = {'title': name, 'image': 'ionicons-folder-32' if is_dir else 'ionicons-ios7-download-outline-32', 'accessory_type': 'disclosure_indicator' if is_dir else 'none', 'is_dir': is_dir, 'path': info['path_display']}
I fixed one bug.
I had to redefine is_dir or else folders were defined as files.
@ui.in_background
def load_folder(self):
infos = list_folder(self.path)
items = []
if self.path != '/':
items.append({'title': '..', 'image': 'ionicons-arrow-up-c-32', 'up': True})
if not infos:
import console
console.alert('Error', 'Could not load folder. Please check if you entered the access token correctly.', 'OK', hide_cancel_button=True)
self.status_label.hidden = True
return
for info in infos:
path = info.get('path_display')
name = os.path.split(path)[1]
if name.startswith('.'):
continue
#is_dir = info.get('is_dir', False)
is_dir = True if info.get('.tag') == 'folder' else False
item = {'title': name, 'image': 'ionicons-folder-32' if is_dir else 'ionicons-ios7-download-outline-32', 'accessory_type': 'disclosure_indicator' if is_dir else 'none', 'is_dir': is_dir, 'path': info['path_display']}
items.append(item)
def c(o1, o2):
u_cmp = -1 * cmp(o1.get('up', False), o2.get('up', False))
if u_cmp != 0:
return u_cmp
d_cmp = -1 * cmp(o1.get('is_dir', False), o2.get('is_dir', False))
if d_cmp == 0:
return cmp(o1.get('path', '').lower(), o2.get('path', '').lower())
return d_cmp
items.sort(cmp=c)
self.tableview.data_source.items = items
self.status_label.hidden = True
self.name = self.path
@bosco Thanks for working on this. Check me on this but I think that the line in download_file
r = requests.post(url, stream=True, headers=headers)
Should be
response = requests.post(url, stream=True, headers=headers)
@ihf I think there may be multiple versions of Dropbox file picker. The one a modified referenced 'r', not 'response' as the object that receives the download file.
Here is my complete download function.
def download_file(path, dest_filename, progress=None):
data = {'path':path}
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Dropbox-API-Arg': json.dumps(data)}
url = 'https://content.dropboxapi.com/2/files/download'
r = requests.post(url, stream=True, headers=headers)
dest_path = os.path.join(os.path.expanduser('~/Documents'), dest_filename)
i = 1
while os.path.exists(dest_path):
base, ext = os.path.splitext(dest_filename)
dest_path = os.path.join(os.path.expanduser('~/Documents'), base + '-' + str(i) + ext)
i += 1
size = r.headers.get('Content-Length', 0)
bytes_written = 0
canceled = False
with open(dest_path, 'w') as f:
for chunk in r.iter_content(1024*10):
f.write(chunk)
bytes_written += len(chunk)
if size > 0 and callable(progress):
p = float(bytes_written) / float(size)
should_cancel = progress(p)
if should_cancel:
canceled = True
break
if canceled:
os.remove(dest_path)
Yes, that would explain it. I used the version that is downloaded if you run ptinstaller within stash. Since that is the repository of Pythonista tools, it would be great if whoever maintains that could update Dropbox File Picker with your version. (Perhaps the first line should be #!python3 in case someone's default is v2)
Here is the complete program that works with python3.
Thanks to @bosco and @ihf
#!python3
# IMPORTANT SETUP INSTRUCTIONS:
#
# 1. Go to http://www.dropbox.com/developers/apps (log in if necessary)
# 2. Select "Create App"
# 3. Select the following settings:
# * "Dropbox API app"
# * "Files and datastores"
# * "(No) My app needs access to files already on Dropbox"
# * "All file types"
# * (Choose any app name)
# 4. On the newly-created app's summary page, click the "Generate"
# button under "Generated access token"
# 5. Copy the generated token (a long string of gibberish) and
# paste it below (replace YOUR_TOKEN_HERE).
# 6. (optional) Open the "wrench" (actions) menu in Pythonista and add
# this script, so you can run it from everywhere.
# Notes:
# All selected files are downloaded into the root folder of the Pythonista
# script library. If a file with the same name already exists, a numeric
# suffix is appended automatically.
TOKEN = 'your token here'
import requests
import urllib
import os
import ui
import functools
import json
def list_folder(folder_path='/'):
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Content-Type': 'application/json' }
if folder_path == '/': folder_path = ''
data = {'path':folder_path}
r = requests.post('https://api.dropboxapi.com/2/files/list_folder', headers=headers, data=json.dumps(data))
result = r.json()
return result.get('entries', None)
def download_file(path, dest_filename, progress=None):
data = {'path':path}
headers = {'Authorization': 'Bearer %s' % (TOKEN,),'Dropbox-API-Arg': json.dumps(data)}
url = 'https://content.dropboxapi.com/2/files/download'
r = requests.post(url, stream=True, headers=headers)
dest_path = os.path.join(os.path.expanduser('~/Documents'), dest_filename)
i = 1
while os.path.exists(dest_path):
base, ext = os.path.splitext(dest_filename)
dest_path = os.path.join(os.path.expanduser('~/Documents'), base + '-' + str(i) + ext)
i += 1
size = int(r.headers.get('Content-Length', 0))
bytes_written = 0
canceled = False
with open(dest_path, 'wb') as f:
for chunk in r.iter_content(1024*10):
f.write(chunk)
bytes_written += len(chunk)
if size > 0 and callable(progress):
p = float(bytes_written) / float(size)
should_cancel = progress(p)
if should_cancel:
canceled = True
break
if canceled:
os.remove(dest_path)
class DropboxView (ui.View):
def __init__(self, path='/'):
tv = ui.TableView()
tv.frame = self.bounds
tv.flex = 'WH'
ds = ui.ListDataSource([])
ds.action = self.item_selected
tv.data_source = ds
tv.delegate = ds
self.tableview = tv
self.add_subview(self.tableview)
self.name = 'Dropbox'
label = ui.Label(frame=self.bounds)
label.flex = 'WH'
label.background_color = (1, 1, 1, 0.95)
label.text = 'Loading...'
label.touch_enabled = True
label.alignment = ui.ALIGN_CENTER
self.path = path
self.add_subview(label)
self.status_label = label
self.canceled = False
def will_close(self):
self.canceled = True
def item_selected(self, sender):
item = sender.items[sender.selected_row]
if item.get('is_dir', False):
self.status_label.text = 'Loading Folder...'
self.status_label.hidden = False
self.path = item['path']
self.load_folder()
elif item.get('up', False):
self.status_label.text = 'Loading Folder...'
self.status_label.hidden = False
self.path = os.path.split(self.path)[0]
self.load_folder()
else:
path = item.get('path')
self.download_file(path)
@ui.in_background
def download_file(self, path):
self.status_label.text = 'Downloading %s...' % (path,)
self.status_label.hidden = False
download_file(path, os.path.split(path)[1], self.download_progress)
self.status_label.hidden = True
def download_progress(self, p):
self.status_label.text = '%i %% Downloaded...' % (p*100,)
return self.canceled
@ui.in_background
def load_folder(self):
infos = list_folder(self.path)
items = []
if self.path != '/':
items.append({'title': '..', 'image': 'ionicons-arrow-up-c-32', 'up': True})
if not infos:
import console
console.alert('Error', 'Could not load folder. Please check if you entered the access token correctly.', 'OK', hide_cancel_button=True)
self.status_label.hidden = True
return
for info in infos:
path = info.get('path_display')
name = os.path.split(path)[1]
if name.startswith('.'):
continue
is_dir = True if info.get('.tag') == 'folder' else False
item = {'title': name, 'image': 'ionicons-folder-32' if is_dir else 'ionicons-ios7-download-outline-32', 'accessory_type': 'disclosure_indicator' if is_dir else 'none', 'is_dir': is_dir, 'path': info['path_display']}
items.append(item)
def cmp(a, b):
return (a > b) - (a < b )
def c(o1, o2):
u_cmp = -1 * cmp(o1.get('up', False), o2.get('up', False))
if u_cmp != 0:
return u_cmp
d_cmp = -1 * cmp(o1.get('is_dir', False), o2.get('is_dir', False))
if d_cmp == 0:
return cmp(o1.get('path', '').lower(), o2.get('path', '').lower())
return d_cmp
items.sort(key=functools.cmp_to_key(c))
self.tableview.data_source.items = items
self.status_label.hidden = True
self.name = self.path
root_view = DropboxView()
root_view.present('fullscreen')
root_view.load_folder()
@enceladus, thanks. Please host it in a repo or at least make it a gist (share - gist in pythonista), and we will put an updated link in Pythonista-Tools.
I would request @bosco to create the gist. He/she has done the main changes. (I have done small edits for running in python3). Anyway if he/she has not created the gist I will create after two/three days mentioning this thread.
@enceladus Please go ahead and make a gist. I hope contribute more in the future when I have more time. Thanks!
Ok. here is the gist. Thanks @bosco .
https://gist.github.com/encela95dus/67fd65aec0c25336ac8e70153ebcf7eb
The gist fails as follows:
Traceback (most recent call last):
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/dp.py", line 127, in load_folder
infos = list_folder(self.path)
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/dp.py", line 42, in list_folder
result = r.json()
File "/var/containers/Bundle/Application/A30CF941-D366-4CE5-BCAA-C22CBEC6C501/Pythonista3.app/Frameworks/Py3Kit.framework/pylib/site-packages/requests/models.py", line 809, in json
return complexjson.loads(self.text, **kwargs)
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/site-packages/simplejson/init.py", line 516, in loads
return _default_decoder.decode(s)
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/site-packages/simplejson/decoder.py", line 370, in decode
obj, end = self.raw_decode(s)
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/site-packages/simplejson/decoder.py", line 400, in raw_decode
return self.scan_once(s, idx=_w(s, idx).end())
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/site-packages/simplejson/scanner.py", line 127, in scan_once
return _scan_once(string, idx)
File "/private/var/mobile/Containers/Shared/AppGroup/B86CEE4E-E4A9-4BB4-9CA7-6E13BDA2C2A4/Pythonista3/Documents/site-packages/simplejson/scanner.py", line 118, in _scan_once
raise JSONDecodeError(errmsg, string, idx)
simplejson.scanner.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
I do not get this error. (I have tested with various file extensions .txt, .py, .zip, .jpg etc). I use beta version of pythonista. May be this is due to different versions of request or json module. May be others could throw some light on this.
Can you try with python2 with original omz version with @bosco changes?
I do not have this "simplejson" module. Rename this module and see if that works.
I think that this is the issue. See the following url/
https://github.com/requests/requests/issues/3052
Hmm..I renamed the SimpleJson folder which was in sitepackages but I get the same error regardless.Funny thing is that this worked before bu now even the version I created from thte earlier posts is failing the same way. It must be somethign other than that simplejson.
Try running the following code to see if simplejson module is imported. Did you restart the pythonista after renaming sinplejson module?
import simplejson
@ihf This script appears to work with python 2 or 3. Try changing #!python3 to #python2. It shouldn't be using simplejson, since json is a core python module. I see now that the your error may be a problem with your requests package. LIne 42 which reads result = r.json coverts the result to json. Line 41 contains json.dumps(data), which converts the arguemnts to a string.
@enceladus Thanks for creating the gist.
in requests.compat:
try:
import simplejson as json
except (ImportError, SyntaxError):
# simplejson does not support Python 3.2, it throws a SyntaxError
# because of u'...' Unicode literals.
import json
If simplejson is present, requests will use it. delete simplejson and fo ce quit pythonista
I am at a loss. Something obviously changed in my setup but I have no clue as to what. I deleted simplejson, emptied trash and restarted Pythonista. Then I recreatied the script using the gist and adding my access key and I get a jsondecode error Expecting value: line 1 column 1 (char 0).
If I run under Python 2.7 I get "No JSON object could be decoded'
import simplejson results in module not found so perhaps now I have a different problem.
I figured it out (sort of). I generated a new access key and now it works. Thank you all.
The new version is added to pythonista-tools. Thanks to all.
script name: Dropbox File Picker V2
Description: Script to import a file from Dropbox into Pythonista - Dropbox V2 API
https://github.com/Pythonista-Tools/Pythonista-Tools/blob/master/Utilities.md
This is great, thank you. While I am dreaming, it would be wonderful if someone in the future were able to make the picker, 2-way so that individual files could be uploaded to dropbox as well as downloaded as is the case now.
@ihf, I do not use Dropbox, but if you have the Dropbox client installed, is it not available when you Share... from Pythonista?
@mikael You are quite right....somehow I never noticed the Share from Pythonista only the Share to. Thanks for pointing that out.