Skip to content

Instantly share code, notes, and snippets.

@SquidDev
Last active December 28, 2016 21:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SquidDev/b46ca5307ce6318962a6 to your computer and use it in GitHub Desktop.
Save SquidDev/b46ca5307ce6318962a6 to your computer and use it in GitHub Desktop.
CC-Dropbox

ComputerCraft-Dropbox

A Dropbox script that can be used in ComputerCraft. Also provides an FS like API which could be wrapped like a virtual file system.

Usage

The syntax is quite simple:

./dropbox_uploader COMMAND [PARAMETERS]...

[%%]: Optional param
<%%>: Required param

Available commands:

  • download <REMOTE> [LOCAL] Download file or directory from Dropbox to a local folder

  • upload <LOCAL> [REMOTE] Upload a file/directory to dropbox

  • delete <REMOTE> Remove a remote file or directory from Dropbox

  • move <REMOTE> <REMOTE> Move or rename a remote file or directory

  • copy <REMOTE> <REMOTE> Copy a remote file or directory

  • mkdir <REMOTE> Create a remote directory on DropBox

  • list [REMOTE] List the contents of the remote Dropbox folder

if dropbox then os.unloadAPI("dropbox") end
if not dropbox then os.loadAPI("dropbox") end
if not dropbox then error("Cannot find dropbox", 0) end
--Prints out the usage for the function
local commands = {
upload = '<LOCAL> [REMOTE]',
download = '<REMOTE> [LOCAL]',
delete = '<REMOTE>',
move = '<SRC> <DEST>',
copy = '<SRC> <DEST>',
mkdir = '<REMOTE_DIR>',
list = '[REMOTE_DIR]',
}
local function usage(command)
if command then
error("Incorrect usage: " .. command .. " " .. commands[command], 0)
end
print([[
Dropbox Uploader 1.0
Usage: COMMAND [PARAMETERS]...
Commands:
upload <LOCAL> [REMOTE]
download <REMOTE> [LOCAL]
delete <REMOTE>
move <SRC> <DEST>
copy <SRC> <DEST>
mkdir <REMOTE_DIR>
list [REMOTE_DIR]
]])
error("Incorrect usage", 0)
end
--Setup variables
local function setup()
write("App key >")
local key = read()
write("App Secret >")
local secret = read()
local app = {
app_key = key,
app_secret = secret,
}
-- These requests a token from online
local tokens = dropbox.requestToken(app)
if not tokens then error("Failed, check app key and secret", 0) end
print("Please open the following url and allow the uploader")
print(tokens.url) -- URL to visit, same as before
print("Press any key to continue")
os.pullEvent("key")
-- This gets an access token
config = dropbox.accessToken(app, tokens)
if not config then error("Failed, please try again", 0) end
return config
end
local config_file = '/.dropbox'
local config
if not fs.exists(config_file) then
config = setup()
-- Save the data
local handle = fs.open(config_file, "w")
handle.write(textutils.serialize(config))
handle.close()
else
local handle = fs.open(config_file, "r")
config = textutils.unserialize(handle.readAll())
handle.close()
end
-- Create a VFS with the specified config
local drop = dropbox.create(config)
local args = {...}
local action = table.remove(args, 1)
if action == "upload" then
-- This is a slightly overcomplicated upload
if #args < 1 or #args > 2 then
usage("upload")
end
local from = shell.resolve(args[1])
if not fs.exists(from) then error("No such file", 0) end
local to = args[2] or fs.combine("", args[1]):gsub("%.%./?", "")
-- Here we check if the file is a directory. If we are a local file and
-- are uploading to a directory then add the file name to the end
if not fs.isDir(from) and drop.isDir(to) then
to = fs.combine(to, fs.getName(from))
end
local queue = { { from, to } }
local todo = {}
while #queue > 0 do
local from, to = unpack(table.remove(queue, 1))
if fs.isDir(from) then
-- Add additional files to the check queue
for _, v in pairs(fs.list(from)) do
table.insert(queue, { fs.combine(from, v), fs.combine(to, v)})
end
else
-- Add additional files to the download queue
-- we execute them in parallel to speed everything up - the limiting
-- factor is network speed, so start them as soon as possible
table.insert(todo, function()
local handle = fs.open(from, "r")
if not handle then
printError("Cannot upload " .. from)
return
end
local remote = drop.open(to, "w")
remote.write(handle.readAll())
handle.close()
remote.close()
end)
end
end
parallel.waitForAll(unpack(todo))
elseif action == "download" then
-- Pretty much the same as above
if #args < 1 or #args > 2 then
usage("download")
end
local from = fs.combine("", args[1]):gsub("%.%./?", "")
local to = shell.resolve(args[2] or args[1])
local isDir = drop.isDir(from)
if fs.isDir(to) and isDir then
to = fs.combine(to, fs.getName(from))
end
local queue = { { from, to, isDir } }
local todo = {}
while #queue > 0 do
local from, to, isDir = unpack(table.remove(queue, 1))
-- This call could be optimised by downloading files
-- straight away rather than gathering a folder tree and then downloading
if isDir then
local meta = drop.getMetadata(from)
if not meta then
printError("Cannot read " .. from)
end
for _, v in pairs(meta.contents) do
-- We pass the is_dir flag as a minor optimisation so we don't check
-- for every file
table.insert(queue, { v.path, fs.combine(to, fs.getName(v.path)), v.is_dir})
end
else
table.insert(todo, function()
local remote = drop.open(from, "r")
if not remote then
printError("Cannot download " .. from)
return
end
local handle = fs.open(to, "w")
handle.write(remote.readAll())
handle.close()
remote.close()
end)
end
end
if #todo == 0 then error("Nothing to download", 0) end
parallel.waitForAll(unpack(todo))
elseif action == "list" or action == "ls" or action == "dir" then
-- Pretty basic, only difference being that
-- we get additional information to
local rows = drop.list(args[1] or "/", true)
local files, dirs = {}, {}
for _, file in pairs(rows) do
table.insert(file.is_dir and dirs or files, fs.getName(file.path))
end
table.sort(files)
table.sort(dirs)
if term.isColour() then
textutils.pagedTabulate(colors.green, dirs, colors.white, files)
else
textutils.pagedTabulate(dirs, files)
end
-- The reset of these are really obvious
elseif action == "copy" or action == "cp" then
if #args ~= 2 then
usage("copy")
end
drop.copy(args[1], args[2])
elseif action == "move" or action == "rename" or action == "mv"then
if #args ~= 2 then
usage("move")
end
drop.move(args[1], args[2])
elseif action == "delete" or action == "remove" then
if #args ~= 1 then
usage("delete")
end
drop.delete(args[1])
elseif action == "mkdir" then
if #args ~= 1 then
usage("mkdir")
end
drop.makeDir(args[1])
else
usage()
end
local API_REQUEST_TOKEN_URL="https://api.dropbox.com/1/oauth/request_token"
local API_USER_AUTH_URL="https://www.dropbox.com/1/oauth/authorize"
local API_ACCESS_TOKEN_URL="https://api.dropbox.com/1/oauth/access_token"
local API_UPLOAD_URL="https://api-content.dropbox.com/1/files_put/auto/"
local API_DOWNLOAD_URL="https://api-content.dropbox.com/1/files/auto"
local API_DELETE_URL="https://api.dropbox.com/1/fileops/delete"
local API_MOVE_URL="https://api.dropbox.com/1/fileops/move"
local API_COPY_URL="https://api.dropbox.com/1/fileops/copy"
local API_METADATA_URL="https://api.dropbox.com/1/metadata/auto/"
local API_MKDIR_URL="https://api.dropbox.com/1/fileops/create_folder"
local APP_CREATE_URL="https://www.dropbox.com/developers/apps"
--- Generate random numbers (9 characters long)
local random = math.random
local function nonce()
local rnd = ""
for i=0,9,1 do rnd = rnd..random(0,9) end
return rnd
end
if not http then error("Cannot access HTTP") end
if not json then
if not fs.exists("json") then shell.run("pastebin get 4nRg9CHU json") end
os.loadAPI("json")
end
local jsonDecode = json.decode
function create(config)
--- Generate default OAuth url parameters
local function oauth()
return "oauth_consumer_key="..config.app_key..
"&oauth_token="..config.token..
"&oauth_signature_method=PLAINTEXT&oauth_signature="..config.app_secret.."%26"..config.token_secret..
"&oauth_timestamp=".."0".."&oauth_nonce"..nonce()
end
--- Gets various metadata for a file/directory
-- https://www.dropbox.com/developers/core/docs#metadata
local function getMetadata(src)
local url = API_METADATA_URL..textutils.urlEncode(src)..'?'..oauth()
local response = http.get(url)
if not response then return nil end
if response.getResponseCode() ~= 200 then
response.close()
return nil
end
local content=jsonDecode(response.readAll())
response.close()
return content
end
local function makeDirectory(destination)
local url = API_MKDIR_URL..'?'..oauth()..'&root=auto&path='..textutils.urlEncode(destination)
local response = http.get(url)
if response then response.close() end
end
local function isDir(file)
local meta = getMetadata(file)
return meta and meta.is_dir
end
local function list(destination, expanded)
local meta = getMetadata(destination)
if not meta then error("Not a directory", 2) end
if not meta.contents then error("API error", 2) end
if expanded then return meta.contents end
local result = {}
for _,file in pairs(meta.contents) do
result[#result + 1] = fs.getName(file.path)
end
return result
end
local function copy(src, destination)
--If destination is directory then copy into directory
local meta = getMetadata(destination)
if not meta then error("No such file", 2) end
if meta.is_dir then
destination = destination..'/'..fs.getName(src)
end
local url = API_COPY_URL .. '?'..oauth()..'&root=auto&from_path='..textutils.urlEncode(src)..'&to_path='..destination
local response = http.get(url)
if not response then error("API error", 2) end
if response.getResponseCode() ~= 200 then
response.close()
error("No such file", 2)
end
response.close()
end
local function move(src, destination)
--If destination is directory then copy into directory
local meta = getMetadata(destination)
if not meta then error("No such file", 2) end
if meta.is_dir then
destination = destination..'/'..fs.getName(src)
end
local url=API_MOVE_URL..'?'..oauth()..'&root=auto&from_path='..textutils.urlEncode(src)..'&to_path='..destination
local response = http.get(url)
if not response then error("API error", 2) end
if response.getResponseCode() ~= 200 then
response.close()
error("No such file", 2)
end
response.close()
end
local function delete(src)
local url = API_DELETE_URL..'?'..oauth()..'&root=auto&path='..textutils.urlEncode(src)
local response = http.get(url)
if response then response.close() end
end
local function open(file, mode)
if mode:find("b") then error("Binary mode not supported", 2) end
if mode == "w" then
local buffer = {}
local size = 0
local function serialize(contents)
local t = type(contents)
if t == "string" then return contents end
if t == "number" or t == "boolean" then return tostring(t) end
-- Technically not accurate.
if t == "table" then return textutils.serialize(contents) end
return ""
end
local function write(contents)
contents = serialize(contents)
buffer[#buffer + 1] = contents
size = size + #contents
end
return {
write = write,
writeLine = function(contents)
write(contents)
write("\n")
end,
close = function()
local url=API_UPLOAD_URL..textutils.urlEncode(file)..'?'..oauth() -- "http://httpbin.org/post" --
local response = http.post(url, table.concat(buffer, ""), {
["Content-Length"] = size,
["Content-Type"] = "application/octet-stream",
})
if response then
response.close()
end
return response ~= nil
end,
}
elseif mode == "r" then
local url = API_DOWNLOAD_URL..textutils.urlEncode(file)..'?'..oauth()
local response = http.get(url)
if response and response.getResponseCode() then
return {
readLine = response.readLine,
readAll = response.readAll,
close = response.close
}
end
else
error("Unsupported mode", 2)
end
end
return {
open = open,
move = move,
list = list,
copy = copy,
delete = delete,
makeDir = makeDirectory,
isDir = isDir,
getMetadata = getMetadata,
}
end
--- Request a token
-- @tparam table config Table composed of app_key and app_secret
-- @treturn table Table composed of the token, secret and url required to authorise
function requestToken(config)
local url = API_REQUEST_TOKEN_URL.."?oauth_consumer_key="..config.app_key.."&oauth_signature_method=PLAINTEXT&oauth_signature="..config.app_secret.."%26".."&oauth_timestamp=".."0".."&oauth_nonce"..nonce()
local response = http.get(url)
if not response then return end
local result_content = response.readAll()
response.close()
local token = string.match(result_content, "oauth_token=\([a-z A-Z 0-9]*\)")
local secret = string.match(result_content, "oauth_token_secret=\([a-z A-Z 0-9]*\)")
if not token or not secret then return end
return {
secret = secret,
token = token,
url = API_USER_AUTH_URL..'?oauth_token='..token
}
end
--- Authorise a token
-- @tparam table config Table composed of app_key and app_secret
-- @tparam table tokens The result of requestToken
function accessToken(config, tokens)
local url=API_ACCESS_TOKEN_URL.."?oauth_consumer_key="..config.app_key..
"&oauth_token="..tokens.token..
"&oauth_signature_method=PLAINTEXT&oauth_signature="..config.app_secret.."%26"..token.secret..
"&oauth_timestamp=0&oauth_nonce="..nonce()
local response=http.get(url)
if not respose then return end
local result_content=response.readAll()
response.close()
local secret = string.match(result_content, "oauth_token_secret=\([a-z A-Z 0-9]*\)")
local token = string.match(result_content, "oauth_token=\([a-z A-Z 0-9]*\)")
if not token or not secret then return end
return {
app_key = config.app_key,
app_secret = config.app_secret,
token = token,
token_secret = secret,
}
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment