12
.gitignore
vendored
|
|
@ -1,24 +1,18 @@
|
|||
#files, the will be ignored by git
|
||||
|
||||
#file with changes
|
||||
json/changes.json
|
||||
|
||||
#c9 files
|
||||
.c9revisions
|
||||
*tmp
|
||||
|
||||
#minified scripts
|
||||
min
|
||||
|
||||
#node modules folder
|
||||
node_modules
|
||||
|
||||
#temporary files folder
|
||||
tmp
|
||||
|
||||
#file used by c9
|
||||
apache-status
|
||||
|
||||
#file used by nodester
|
||||
.nodester.appconfig
|
||||
|
||||
#log file
|
||||
log.txt
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ language: node_js
|
|||
node_js:
|
||||
- 0.6
|
||||
- 0.8
|
||||
- 0.9
|
||||
- 0.10
|
||||
notifications:
|
||||
#webhooks:
|
||||
#http://requestb.in/12h5bl71
|
||||
|
|
|
|||
16
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
If you would like to contribute - send pull request to dev branch.
|
||||
Getting dev version of **Cloud Commander**:
|
||||
|
||||
git clone git://github.com/coderaiser/cloudcmd.git
|
||||
git checkout dev
|
||||
|
||||
or by [link](https://github.com/coderaiser/cloudcmd/tree/dev "Dev version").
|
||||
|
||||
It is possible thet dev version Cloud Commander will needed dev version of Minify,
|
||||
so to get it you should type a couple more commands:
|
||||
|
||||
cd node_modules
|
||||
rm -rf minify
|
||||
git clone git://github.com/coderaiser/minify
|
||||
cd minify
|
||||
git checkout dev
|
||||
116
ChangeLog
|
|
@ -1,4 +1,100 @@
|
|||
2012.03.01, Version 0.1.9
|
||||
2012.04.22, v0.2.0
|
||||
|
||||
* Added alerting about errors.
|
||||
|
||||
* Removed Listeners array for DOM.
|
||||
|
||||
* Added ability create folders.
|
||||
|
||||
* Added filepicker.
|
||||
|
||||
* Added ability to load files to cloud
|
||||
and get them out to file system.
|
||||
|
||||
* Added SSL suport.
|
||||
|
||||
* jquery updated to v1.9.1.
|
||||
|
||||
* Fixed bug with multiple click events on f3 and f4 buttons.
|
||||
|
||||
* Added ability to move files.
|
||||
|
||||
* Added ability to degradate to http from https.
|
||||
|
||||
* Fixed bug of multiple call of sockets.
|
||||
|
||||
* If create directory command executed - loading spinner
|
||||
would be on top.
|
||||
|
||||
* Refactored getShortSize.
|
||||
|
||||
* Added update of size on file changing in editor.
|
||||
|
||||
* Removed cache control of fs resour.
|
||||
|
||||
* Added ability to copy files.
|
||||
|
||||
* Fixed bug with saving json to localStorage, it's always
|
||||
writed root directory.
|
||||
|
||||
* Moved out set current file from cloudfunc to client.js.
|
||||
|
||||
* Changed the way file table building. From now templating used.
|
||||
|
||||
* Changed sync reading of certs to async.
|
||||
|
||||
* Updated dropbox to v0.9.2 and moved to packege.json
|
||||
from storage folder.
|
||||
|
||||
* Updated socket.io to v0.9.14.
|
||||
|
||||
* Fixed Util.time and Util.timeEnd
|
||||
|
||||
* Added ability to select files with Insert key.
|
||||
|
||||
* Added ability to delete selected files from DOM.
|
||||
|
||||
* Added ability to recursivly get current folder size.
|
||||
|
||||
* Added ability to get directory size, when space button
|
||||
is pressed and current file is directory.
|
||||
|
||||
* Added ability to select all files on Ctrl + A
|
||||
|
||||
* Fixed the href generation of renamed file.
|
||||
|
||||
* Renamed promptNewFolder -> promptNewDir'.
|
||||
|
||||
* Added function getSelectedNames.
|
||||
|
||||
* Added ability to delete file when f8 key pressed.
|
||||
|
||||
* Fixed bug with selectecting root directory with space.
|
||||
|
||||
* Fixed bug with processing selected root directory.
|
||||
|
||||
* Added ability to delete files.
|
||||
|
||||
* Removed modules cache.js.
|
||||
|
||||
* Fixed bug with writing new file.
|
||||
|
||||
* Updated jquery to v2.0.0.
|
||||
|
||||
* Improved CloudFunc module.
|
||||
|
||||
* Fixed selection style in opera.
|
||||
|
||||
* Fixed header of api fs GET.
|
||||
|
||||
* Fixed bug with sending response on
|
||||
query other then json on /fs url.
|
||||
|
||||
* Fixed bug with getting back to
|
||||
directory where file was removed.
|
||||
|
||||
|
||||
2012.03.01, v0.1.9
|
||||
|
||||
* Changed the way of getting github application id
|
||||
(now it's just from config, rest api removed).
|
||||
|
|
@ -192,7 +288,7 @@ CloudCmd REST API.
|
|||
* Added help screen (on F1 after viewer loads).
|
||||
|
||||
|
||||
2012.12.12, Version 0.1.8
|
||||
2012.12.12, v0.1.8
|
||||
|
||||
* Added ability to shutdown Cloud Commander
|
||||
thru terminal command: "cloudcmd exit"
|
||||
|
|
@ -385,7 +481,7 @@ Just propose install git and clone from github repo.
|
|||
* Fixed bug with appcache.
|
||||
|
||||
|
||||
2012.10.01, Version 0.1.7
|
||||
2012.10.01, v0.1.7
|
||||
|
||||
* Changed name of menu files, fixed npm and jitsu
|
||||
problem with menu showing.
|
||||
|
|
@ -471,7 +567,7 @@ disabled in browsers.
|
|||
* Added fix of 866 charset.
|
||||
|
||||
|
||||
2012.08.24, Version 0.1.6
|
||||
2012.08.24, v0.1.6
|
||||
|
||||
* From now jsload, cssload and anyload suport arrays.
|
||||
|
||||
|
|
@ -524,7 +620,7 @@ current, file that was previously current must be unset automatically.
|
|||
* Added ability to download files.
|
||||
|
||||
|
||||
2012.08.06, Version 0.1.5
|
||||
2012.08.06, v0.1.5
|
||||
|
||||
* Added tab support.
|
||||
|
||||
|
|
@ -564,7 +660,7 @@ pressed couple times and CodeMirror not loaded fully.
|
|||
and passive panels and show/hide any of panels.
|
||||
|
||||
|
||||
2012.07.27, Version 0.1.4
|
||||
2012.07.27, v0.1.4
|
||||
|
||||
* Added local version of Droids font for offline mode
|
||||
|
||||
|
|
@ -600,7 +696,7 @@ be more faster, DOM, short, simple and logical.
|
|||
* Added ability remove not loaded scripts from DOM.
|
||||
|
||||
|
||||
2012.07.19, Version 0.1.3
|
||||
2012.07.19, v0.1.3
|
||||
|
||||
* Fixed bug with nodester (jitsu env.HOME make him go done).
|
||||
|
||||
|
|
@ -633,7 +729,7 @@ setted b char: "0b".
|
|||
* Added supporting of Russian language in directory names.
|
||||
|
||||
|
||||
2012.07.14, Version 0.1.2
|
||||
2012.07.14, v0.1.2
|
||||
|
||||
* Added suport of jitsu.
|
||||
|
||||
|
|
@ -644,7 +740,7 @@ setted b char: "0b".
|
|||
* Changed the minimize function calls accroding to Minify 0.1.2 changes.
|
||||
|
||||
|
||||
2012.07.11, Version 0.1.1
|
||||
2012.07.11, v0.1.1
|
||||
|
||||
* Added onerror parametr to anyload in Clinet Side, so now we can process situation when
|
||||
js-script(or something other) has not loaded.
|
||||
|
|
@ -663,4 +759,4 @@ post-processing functions passed like this {'client.js': function(){}}
|
|||
* Added ability to read file data from Minify Cache, without writing to disk
|
||||
|
||||
* Changed the passing MoreProcessing agrument to jsScripts function Minify module,
|
||||
no it passes with a file name, and js file name writing only once.
|
||||
no it passes with a file name, and js file name writing only once.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,64 @@
|
|||
**2012.03.01**
|
||||
2012.04.22, v0.2.0
|
||||
===============
|
||||
Весна в разгаре, на деревьях появляются почки, наконец-то начинает теплеть.
|
||||
А это значит, что пришло время выпускать на свободу новую версию Командира.
|
||||
|
||||
Много интересных вещей произошло и изменилось со времени прошлого релиза.
|
||||
Сейчас мы рассмотрим поближе, что же именно изменилось. Итак начнём.
|
||||
|
||||
**Добавленна внешняя возможность:**
|
||||
- Всплывающее сообщение об ошибках
|
||||
- Создание папки.
|
||||
- FilePicker.
|
||||
- загружать файлы в облако и скачивать их в файловую систему.
|
||||
- SSL.
|
||||
- Перемещения файлов.
|
||||
- Перехода на http, если https-сервер запустить не удалось.
|
||||
- Если создаётся директория индикатор загрузки будет сверху.
|
||||
- Обновления размера файла после изминения оного в редакторе.
|
||||
- Копирования файлов.
|
||||
- Выделения файлов клавишей Insert.
|
||||
- Получения размера текущей папки при нажатии клавиши Space.
|
||||
- Выделение всех файлов по Ctrl + A.
|
||||
- Удаления файла по F8.
|
||||
- Удаления нескольких файлов.
|
||||
|
||||
**Исправленные ошибки:**
|
||||
- Несколько обработчиков клика по нажатию клавиш F3 и F4.
|
||||
- Множественный вызов Sockets.
|
||||
- Запись json в localStorage: всегда обновлялось содержимое корневой директории.
|
||||
- В функциях Util.time и Util.timeEnd.
|
||||
- Генереация href файла, который переименовывается.
|
||||
- Выделение коревой директории клавишей Space
|
||||
- Обработка выделенной корневой директории.
|
||||
- Создание нового файла.
|
||||
- Стиль выделения в Опере.
|
||||
- Заголовок выдачи REST-фукнции fs метода GET.
|
||||
- Отправка данных в ответ на запрос отличный от ?json в пути /url.
|
||||
- Возвращение в папку, из которой был удален файл.
|
||||
|
||||
**Обновлены:**
|
||||
- jquery до версии v2.0.0
|
||||
- dropbox до версии v0.9.2
|
||||
- socket.io до версии v0.9.14.
|
||||
|
||||
**Внутренние изминения:**
|
||||
- Удален массив Listeners из модуля DOM.
|
||||
- Переделан и оптимизирована функция getShortSize в CloudFunc.
|
||||
- Контроль генерации заголовка кеша перенесен в модуль Main.
|
||||
- Установка текущего файла перенесена в Client из CloudFunc.
|
||||
- Изменён способ построения таблицы файлов. Теперь используются шаблоны.
|
||||
- Загрузка SSL-сертификатов теперь асинхронная.
|
||||
- Модуль DropBox перенесён в packege.json из папки Storage.
|
||||
- Удаления выделеного файла из DOM.
|
||||
- Рекурсивного получения размера текущей папки.
|
||||
- Переименована функция promptNewFolder -> promptNewDir.
|
||||
- Добавлена функция getSelectedNames.
|
||||
- Удален модуль Сache.
|
||||
- Оптимизирован модуль CloudFunc.
|
||||
|
||||
2012.03.01, v0.1.9
|
||||
===============
|
||||
Сегодня, в первый день весны, вышла новая версия облачного менеджера файлов: 0.1.9.
|
||||
В связи с этим, есть двойной повод порадоватся: наконец, пришла весна,
|
||||
с полной готовностью согревать серца жителей, после зимних холодов,
|
||||
|
|
|
|||
85
README.md
|
|
@ -1,11 +1,22 @@
|
|||
Cloud Commander [](http://travis-ci.org/coderaiser/cloudcmd)
|
||||
Cloud Commander v0.2.0 [![NPM version][NPMIMGURL]][NPMURL] [![Dependency Status][DependencyStatusIMGURL]][DependencyStatusURL] [![Build Status][BuildStatusIMGURL]][BuildStatusURL]
|
||||
===============
|
||||
[![Flattr][FlattrIMGURL]][FlattrURL]
|
||||
[NPMIMGURL]: https://badge.fury.io/js/cloudcmd.png
|
||||
[BuildStatusIMGURL]: https://secure.travis-ci.org/coderaiser/cloudcmd.png?branch=master
|
||||
[DependencyStatusIMGURL]: https://gemnasium.com/coderaiser/cloudcmd.png
|
||||
[FlattrIMGURL]: http://api.flattr.com/button/flattr-badge-large.png
|
||||
[NPMURL]: http://badge.fury.io/js/cloudcmd
|
||||
[BuildStatusURL]: http://travis-ci.org/coderaiser/cloudcmd "Build Status"
|
||||
[DependencyStatusURL]: https://gemnasium.com/coderaiser/cloudcmd "Dependency Status"
|
||||
[FlattrURL]: https://flattr.com/submit/auto?user_id=coderaiser&url=github.com/coderaiser/cloudcmd&title=cloudcmd&language=&tags=github&category=software
|
||||
|
||||
**Cloud Commander** - user friendly cloud file manager.
|
||||
DEMO:
|
||||
[cloudfoundry] (http://cloudcmd.cloudfoundry.com "cloudfoundry"),
|
||||
[appfog] (http://cloudcmd.aws.af.cm "appfog").
|
||||
[cloudfoundry] (https://cloudcmd.cloudfoundry.com "cloudfoundry"),
|
||||
[appfog] (https://cloudcmd.aws.af.cm "appfog"),
|
||||
[jitsu] (https://cloudcmd.jit.su "jitsu").
|
||||
|
||||
Google PageSpeed Score : [100](http://developers.google.com/speed/pagespeed/insights#url=http_3A_2F_2Fcloudcmd.cloudfoundry.com_2F&mobile=false "score") (out of 100)
|
||||
Google PageSpeed Score : [100](//developers.google.com/speed/pagespeed/insights#url=http_3A_2F_2Fcloudcmd.aws.af.cm_2F&mobile=false "score") (out of 100)
|
||||
(or 96 if js or css minification disabled in config.json).
|
||||
|
||||

|
||||
|
|
@ -40,15 +51,18 @@ There is a short list:
|
|||
- **Ctrl + d** - clear local cache (wich contains dir contents)
|
||||
- **Alt + q** - disable key bindings
|
||||
- **Alt + s** - get all key bindings back
|
||||
- **Ctrl + A** - select all files in a panel
|
||||
- **up, down, enter** - filesystem navigation
|
||||
- **Tab** - move thru panels
|
||||
- **Page Up** - up on one page
|
||||
- **Page Down** - down on one page
|
||||
- **Home** - to begin of list
|
||||
- **End** - to end of list
|
||||
- **Shift + F10** - show context menu
|
||||
- **F8, Delete** - remove current file
|
||||
- **Shift + Delete** - remove without prompt
|
||||
- **Insert** - select current file
|
||||
- **F2** - rename current file
|
||||
- **Alt + g** - authorization in GitHub
|
||||
- **Shift + F10** - show context menu
|
||||
|
||||
Viewer's hot keys
|
||||
---------------
|
||||
|
|
@ -70,12 +84,8 @@ Right mouse click button show context menu with items:
|
|||
- Rename
|
||||
- Delete
|
||||
- Upload to (Dropbox, Github, GDrive)
|
||||
- Downloud
|
||||
|
||||
|
||||
Documentation
|
||||
---------------
|
||||
JS Doc documentation could be found in [http://jsdoc.info/coderaiser/cloudcmd/](http://jsdoc.info/coderaiser/cloudcmd/)
|
||||
- Download
|
||||
- New (File, Dir, from cloud)
|
||||
|
||||
Installing
|
||||
---------------
|
||||
|
|
@ -107,11 +117,46 @@ All main configuration could be done thrue config.json.
|
|||
"server" : true, /* server mode or testing mode */
|
||||
"logs" : false, /* logs or console ouput */
|
||||
"socket" : true /* enable web sockets */
|
||||
"port" : 80, /* Cloud Commander port */
|
||||
"port" : 80, /* http port */
|
||||
"sslPort" : 443, /* https port */
|
||||
"ip" : "127.0.0.1", /* Cloud Commander IP */
|
||||
"ssl" : true /* should use https? */
|
||||
"rest" : true /* enable rest interface */
|
||||
}
|
||||
```
|
||||
|
||||
Server
|
||||
---------------
|
||||
Standard practices say no non-root process gets to talk to
|
||||
the Internet on a port less than 1024. Anyway I suggest you
|
||||
to start Cloud Commander as non-root. How it could be solved?
|
||||
There is a couple easy and fast ways. One of them is port forwarding by iptables.
|
||||
|
||||
```sh
|
||||
@:/tmp/cloudcmd (dev) $ su iptables -t nat -L # look rules before
|
||||
@:/tmp/cloudcmd (dev) $ su iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 8000
|
||||
@:/tmp/cloudcmd (dev) $ su iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 4430
|
||||
@:/tmp/cloudcmd (dev) $ su iptables -t nat -L # look reles after
|
||||
```
|
||||
You should see somethins like this ( **8000** and **4430** should be in config as **port** and **sslPort** )
|
||||
|
||||
target prot opt source destination
|
||||
REDIRECT tcp -- anywhere anywhere tcp dpt:http redir ports 8000
|
||||
REDIRECT tcp -- anywhere anywhere tcp dpt:https redir ports 4430
|
||||
|
||||
If you would want to get things back just clear rules ( **1** and **2** it's rules numbers,
|
||||
in your list they could differ).
|
||||
|
||||
```sh
|
||||
@:/tmp/cloudcmd (dev) $ su iptables -t nat -D PREROUTING 1
|
||||
@:/tmp/cloudcmd (dev) $ su iptables -t nat -D PREROUTING 2
|
||||
```
|
||||
|
||||
To run Cloud Commander as daemon in linux you could set **log** to true in config and
|
||||
do something like this:
|
||||
|
||||
nohup node cloudcmd
|
||||
|
||||
Authorization
|
||||
---------------
|
||||
Thru openID Cloud Commander could authorize clients on GitHub.
|
||||
|
|
@ -210,6 +255,20 @@ so to get it you should type a couple more commands:
|
|||
git clone git://github.com/coderaiser/minify
|
||||
git checkout dev
|
||||
|
||||
Version history
|
||||
---------------
|
||||
- *2012.04.22*, **[v0.2.0](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.2.0.zip)**
|
||||
- *2012.03.01*, **[v0.1.9](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.9.zip)**
|
||||
- *2012.12.12*, **[v0.1.8](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.8.zip)**
|
||||
- *2012.10.01*, **[v0.1.7](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.7.zip)**
|
||||
- *2012.08.24*, **[v0.1.6](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.6.zip)**
|
||||
- *2012.08.06*, **[v0.1.5](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.5.zip)**
|
||||
- *2012.07.27*, **[v0.1.4](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.4.zip)**
|
||||
- *2012.07.19*, **[v0.1.3](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.3.zip)**
|
||||
- *2012.07.14*, **[v0.1.2](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.2.zip)**
|
||||
- *2012.07.11*, **[v0.1.1](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.1.zip)**
|
||||
- *2012.00.00*, **[v0.1.0](//github.com/coderaiser/cloudcmd-archive/raw/master/cloudcmd-v0.1.0.zip)**
|
||||
|
||||
Special Thanks
|
||||
---------------
|
||||
[Elena Zalitok](http://vk.com/politilena "Elena Zalitok") for logo.
|
||||
|
|
|
|||
125
cloudcmd.js
|
|
@ -23,12 +23,21 @@
|
|||
|
||||
INDEX = HTMLDIR + 'index.html',
|
||||
CONFIG_PATH = JSONDIR + 'config.json',
|
||||
|
||||
CA = DIR + 'ssl/sub.class1.server.ca.pem',
|
||||
KEY = DIR + 'ssl/ssl.key',
|
||||
CERT = DIR + 'ssl/ssl.crt',
|
||||
|
||||
FILE_TMPL = HTMLDIR + 'file.html',
|
||||
|
||||
PATH_TMPL = HTMLDIR + 'path.html',
|
||||
|
||||
FileTemplate, PathTemplate,
|
||||
|
||||
FS = CloudFunc.FS;
|
||||
|
||||
/* reinit main dir os if we on
|
||||
* Win32 should be backslashes */
|
||||
/* reinit main dir os if we on Win32 should be backslashes */
|
||||
DIR = main.DIR;
|
||||
|
||||
|
||||
readConfig(init);
|
||||
|
||||
|
||||
|
|
@ -36,8 +45,7 @@
|
|||
* additional processing of index file
|
||||
*/
|
||||
function indexProcessing(pData){
|
||||
var lReplace_s,
|
||||
lData = pData.data,
|
||||
var lData = pData.data,
|
||||
lAdditional = pData.additional;
|
||||
|
||||
/*
|
||||
|
|
@ -46,9 +54,9 @@
|
|||
* минифицированый
|
||||
*/
|
||||
if(Minify.allowed.css){
|
||||
var lPath = '/' + Util.removeStr(Minify.MinFolder, DIR);
|
||||
lReplace_s = '<link rel=stylesheet href="/css/reset.css">';
|
||||
lData = Util.removeStr(lData, lReplace_s)
|
||||
var lPath = '/' + Util.removeStr(Minify.MinFolder, DIR),
|
||||
lReplace = '<link rel=stylesheet href="/css/reset.css">';
|
||||
lData = Util.removeStr(lData, lReplace)
|
||||
.replace('/css/style.css', lPath + 'all.min.css');
|
||||
}
|
||||
|
||||
|
|
@ -73,9 +81,14 @@
|
|||
* init and process of appcache if it allowed in config
|
||||
*/
|
||||
function appCacheProcessing(){
|
||||
var lFiles = [
|
||||
{'//themes.googleusercontent.com/static/fonts/droidsansmono/v4/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff' : './font/DroidSansMono.woff'},
|
||||
{'//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js' : './lib/client/jquery.js'}];
|
||||
var lFONT_REMOTE = '//themes.googleusercontent.com/static/fonts/droidsansmono/v4/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff',
|
||||
lFONT_LOCAL = './font/DroidSansMono.woff',
|
||||
lJQUERY_REMOTE = 'http://code.jquery.com/jquery-2.0.0.min.js',
|
||||
lJQUERY_LOCAL = './lib/client/jquery.js',
|
||||
lFiles = [{}, {}];
|
||||
|
||||
lFiles[0][lFONT_REMOTE] = lFONT_LOCAL;
|
||||
lFiles[1][lJQUERY_REMOTE] = lJQUERY_LOCAL;
|
||||
|
||||
if(Config.minification.css)
|
||||
lFiles.push('node_modules/minify/min/all.min.css');
|
||||
|
|
@ -121,7 +134,7 @@
|
|||
|
||||
/**
|
||||
* rest interface
|
||||
* @pConnectionData {request, responce}
|
||||
* @pParams pConnectionData {request, responce}
|
||||
*/
|
||||
function rest(pConnectionData){
|
||||
return Util.exec(main.rest, pConnectionData);
|
||||
|
|
@ -151,8 +164,7 @@
|
|||
/* if command line parameter testing resolved
|
||||
* setting config to testing, so server
|
||||
* not created, just init and
|
||||
* all logs writed to screen
|
||||
*/
|
||||
* all logs writed to screen */
|
||||
var lArg = process.argv;
|
||||
lArg = lArg[lArg.length - 1];
|
||||
if ( lArg === 'test' || lArg === 'test\r') {
|
||||
|
|
@ -165,22 +177,46 @@
|
|||
'from now all logs will be writed to log.txt');
|
||||
writeLogsToFile();
|
||||
}
|
||||
}
|
||||
|
||||
if(Config.server)
|
||||
fs.watch(CONFIG_PATH, function(){
|
||||
/* every catch up - calling twice */
|
||||
setTimeout(function() {
|
||||
readConfig();
|
||||
}, 1000);
|
||||
if(Config.server)
|
||||
fs.watch(CONFIG_PATH, function(){
|
||||
/* every catch up - calling twice */
|
||||
setTimeout(function() {
|
||||
readConfig();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
var lParams = {
|
||||
appcache : appCacheProcessing,
|
||||
minimize : minimize,
|
||||
rest : rest,
|
||||
route : route
|
||||
},
|
||||
lFiles = [FILE_TMPL, PATH_TMPL];
|
||||
|
||||
if(Config.ssl)
|
||||
lFiles.push(CA, KEY, CERT);
|
||||
|
||||
main.readFiles(lFiles, function(pErrors, pFiles){
|
||||
if(pErrors)
|
||||
Util.log(pErrors);
|
||||
else{
|
||||
FileTemplate = pFiles[FILE_TMPL].toString();
|
||||
PathTemplate = pFiles[PATH_TMPL].toString();
|
||||
|
||||
if(Config.ssl)
|
||||
lParams.ssl = {
|
||||
ca : pFiles[CA],
|
||||
key : pFiles[KEY],
|
||||
cert : pFiles[CERT]
|
||||
};
|
||||
|
||||
server.start(lParams);
|
||||
}
|
||||
});
|
||||
|
||||
server.start({
|
||||
appcache : appCacheProcessing,
|
||||
minimize : minimize,
|
||||
rest : rest,
|
||||
route : route
|
||||
});
|
||||
}
|
||||
else
|
||||
Util.log('read error: config.json');
|
||||
}
|
||||
|
||||
function readConfig(pCallBack){
|
||||
|
|
@ -220,12 +256,16 @@
|
|||
if( Util.strCmp(p.name, ['/auth', '/auth/github']) ){
|
||||
Util.log('* Routing' +
|
||||
'-> ' + p.name);
|
||||
pParams.name = main.HTMLDIR + p.name + '.html';
|
||||
lRet = main.sendFile( pParams );
|
||||
|
||||
pParams.name = main.HTMLDIR + p.name + '.html';
|
||||
lRet = main.sendFile( pParams );
|
||||
}
|
||||
else if( Util.isContainStr(p.name, FS) || Util.strCmp( p.name, '/') ){
|
||||
if( Util.isContainStr(p.name, 'no-js/') )
|
||||
return noJSTMPRedirection(pParams);
|
||||
else if( Util.isContainStrAtBegin(p.name, FS) || Util.strCmp( p.name, '/') ){
|
||||
|
||||
if( Util.isContainStrAtBegin(p.name, FS + 'no-js/') ){
|
||||
var lURL = Util.removeStr(pParams.name, 'no-js/');
|
||||
return main.redirect(pParams, lURL);
|
||||
}
|
||||
|
||||
lRet = sendCommanderContent(p);
|
||||
}
|
||||
|
|
@ -241,7 +281,7 @@
|
|||
var lRet = main.checkParams(pParams);
|
||||
if(lRet){
|
||||
var p = pParams;
|
||||
p.name = Util.removeStr(p.name, CloudFunc.FS) || main.SLASH;
|
||||
p.name = Util.removeStrOneTime(p.name, CloudFunc.FS) || main.SLASH;
|
||||
|
||||
fs.stat(p.name, function(pError, pStat){
|
||||
if(!pError)
|
||||
|
|
@ -260,6 +300,7 @@
|
|||
|
||||
function processCommanderContent(pParams){
|
||||
var lRet = main.checkParams(pParams);
|
||||
|
||||
if(lRet){
|
||||
var p = pParams;
|
||||
main.commander.getDirContent(p.name, function(pError, pJSON){
|
||||
|
|
@ -270,11 +311,11 @@
|
|||
p.name +='.json';
|
||||
main.sendResponse(p);
|
||||
}
|
||||
else if(!lQuery){ /* get back html*/
|
||||
else{ /* get back html*/
|
||||
p.name = Minify.allowed.html ? Minify.getName(INDEX) : INDEX;
|
||||
fs.readFile(p.name, function(pError, pData){
|
||||
if(!pError){
|
||||
var lPanel = CloudFunc.buildFromJSON(pJSON),
|
||||
var lPanel = CloudFunc.buildFromJSON(pJSON, FileTemplate, PathTemplate),
|
||||
lList = '<ul id=left class=panel>' + lPanel + '</ul>' +
|
||||
'<ul id=right class=panel>' + lPanel + '</ul>';
|
||||
|
||||
|
|
@ -294,16 +335,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function noJSTMPRedirection(pParams){
|
||||
var MOVED_PERMANENTLY = 301,
|
||||
lPath = Util.removeStr(pParams.name, 'no-js/');
|
||||
|
||||
pParams.response.writeHead(MOVED_PERMANENTLY, {'Location': lPath});
|
||||
pParams.response.end();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* function sets stdout to file log.txt */
|
||||
function writeLogsToFile(){
|
||||
|
|
|
|||
|
|
@ -26,7 +26,12 @@ body { margin: 0; font-size: 1em; line-height: 1.4; }
|
|||
::selection { background: #fe57a1; color: #fff; text-shadow: none; }
|
||||
*/
|
||||
::-moz-selection{ text-shadow: none; opacity: 0;}
|
||||
::selection { text-shadow: none; opacity: 0;}
|
||||
::selection {
|
||||
text-shadow: none;
|
||||
opacity: 0;
|
||||
background-color:white; /* opera */
|
||||
}
|
||||
|
||||
|
||||
/* =============================================================================
|
||||
Links
|
||||
|
|
|
|||
703
css/style.css
|
|
@ -1,352 +1,353 @@
|
|||
/*
|
||||
@import url(//fonts.googleapis.com/css?family=Droid+Sans+Mono);
|
||||
*/
|
||||
|
||||
/* символьный шрифт от гитхаба*/
|
||||
@font-face {
|
||||
font-family: "GeneralFoundicons";
|
||||
src: url('/font/general_foundicons.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* http://fontello.com/ */
|
||||
@font-face {
|
||||
font-family: 'Fontello';
|
||||
src: url("/font/fontello.eot");
|
||||
src: url("/font/fontello.eot?#iefix") format('embedded-opentype'), url("/font/fontello.woff") format('woff'), url("/font/fontello.ttf") format('truetype'), url("/font/fontello.svg#cloudcmd") format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Droid Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: local('Droid Sans Mono'), local('DroidSansMono'), url('http://themes.googleusercontent.com/static/fonts/droidsansmono/v4/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff') format('woff');
|
||||
}
|
||||
|
||||
body{
|
||||
font:16px "Droid Sans Mono";
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
.path-icon{
|
||||
position: relative;
|
||||
top: 3px;
|
||||
left: -4px;
|
||||
display: inline-block;
|
||||
/* размер иконки и позиция на png-файле*/
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
font-family:'FoundationIconsGeneralEnclosed';
|
||||
font-size:30px;
|
||||
color: #46A4C3;/*#55BF3F; green*/
|
||||
text-shadow:black 0 2px 1px;
|
||||
}
|
||||
.path-icon:hover{
|
||||
cursor:pointer;
|
||||
}
|
||||
.path-icon:active{
|
||||
position: relative;
|
||||
top: 4px;
|
||||
text-shadow:black 0 0 1px;
|
||||
}
|
||||
.icon{
|
||||
display:inline-block;
|
||||
width:16px;
|
||||
height:16px;
|
||||
margin-left:0.5%;
|
||||
/* font-family: 'GeneralFoundicons'; */
|
||||
font-family: 'Fontello';
|
||||
}
|
||||
.error::before{
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
color: rgb(222, 41, 41);
|
||||
cursor :default;
|
||||
content: '\2757';
|
||||
}
|
||||
.loading{
|
||||
position:relative;
|
||||
top:1px;
|
||||
background:url(/img/spinner.gif);
|
||||
}
|
||||
.error:hover{
|
||||
color:rgb(222, 41, 41);
|
||||
color:rgba(222, 41, 41, 0.81);
|
||||
}
|
||||
.refresh-icon{
|
||||
background:url(/img/panel_refresh.png) no-repeat;
|
||||
}
|
||||
.refresh-icon:active{
|
||||
/*background-position-y: -15px;*/
|
||||
background:url(/img/panel_refresh.png) 0 -15px no-repeat;
|
||||
}
|
||||
|
||||
.cmd-button{
|
||||
width: 10%;
|
||||
margin: 20px 2px 0 2px;
|
||||
overflow: hidden;
|
||||
color: rgb(49,123,249);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background-color: white;
|
||||
border: 1.5px solid rgba(49,123,249,.40);
|
||||
}
|
||||
|
||||
.cmd-button:hover{
|
||||
border: 1.5px solid rgb(0,0,0);
|
||||
}
|
||||
|
||||
.cmd-button:active{
|
||||
color: white;
|
||||
background-color: rgb(49,123,249);
|
||||
}
|
||||
|
||||
.clear-cache{
|
||||
margin-right: 6px;
|
||||
margin-left: 7px;
|
||||
background:url(/img/console_clear.png) -4px -4px no-repeat;
|
||||
}
|
||||
.clear-cache:active{
|
||||
top:5px;
|
||||
}
|
||||
|
||||
.links{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.mini-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
left: -5px;
|
||||
float: left;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 6px;
|
||||
/* отступ перед картинкой
|
||||
* для нормального отображения
|
||||
* рамки
|
||||
*/
|
||||
}
|
||||
/* уменьшаем отступ
|
||||
* между между иконкой и
|
||||
* именем файла во время
|
||||
* установления курсора
|
||||
*/
|
||||
.current-file > .mini-icon{
|
||||
left: -6px;
|
||||
}
|
||||
/* freeupex */
|
||||
.directory{
|
||||
/*list-style-image*/
|
||||
background-image:url('/img/dir.png');
|
||||
background-position: 0 0;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.text-file{
|
||||
/*list-style-image*/
|
||||
background-image:url('/img/txt.png');
|
||||
background-position: 0 0;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#fm{
|
||||
height: 90%;
|
||||
margin: 26px 26px 0 26px;
|
||||
}
|
||||
.fm-header{
|
||||
font-weight: bold;
|
||||
}
|
||||
#path{
|
||||
margin-left:1.5%;
|
||||
}
|
||||
.left, #left{
|
||||
float:left;
|
||||
}
|
||||
/* фон файла, на котором курсор*/
|
||||
.current-file{
|
||||
border: 1.5px solid rgba(49, 123, 249, .40);
|
||||
}
|
||||
.selected-file{
|
||||
color:white;
|
||||
background-color: rgb(49, 123, 249);
|
||||
background-color: rgba(49, 123, 249, .40);
|
||||
}
|
||||
|
||||
.right, #right{
|
||||
float:right;
|
||||
}
|
||||
.panel{
|
||||
width: 46%;
|
||||
overflow-y: auto;
|
||||
border: 1.5px solid rgba(49, 123, 249, .40);
|
||||
}
|
||||
#keyspanel{
|
||||
text-align: center;
|
||||
}
|
||||
/* информация о файлах и папках*/
|
||||
.name{
|
||||
float: left;
|
||||
width: 37%;
|
||||
/* если длина имени файла больше 16 символов
|
||||
* отрезаем лишнее, оставляя лишь 16,
|
||||
* и добавляем две точки и тайтл
|
||||
*/
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.size{
|
||||
float:left;
|
||||
width:16%;
|
||||
/* Ставим отступ, что бы
|
||||
* size не налазил на uid
|
||||
* (owner)
|
||||
*/
|
||||
margin-right: 27px;
|
||||
/* Ставим выравнивание,
|
||||
* что бы размер был в
|
||||
* одной ровной колонке
|
||||
*/
|
||||
text-align: right;
|
||||
}
|
||||
.mode{
|
||||
float: right;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
/* changin ul to panel for high speed parsing*/
|
||||
.panel, li{
|
||||
list-style-type:none;
|
||||
/* making cursor just arrow,
|
||||
* not text editing cursor
|
||||
*/
|
||||
cursor:default;
|
||||
}
|
||||
button{
|
||||
width:10%;
|
||||
}
|
||||
a{
|
||||
text-decoration:none;
|
||||
}
|
||||
a:hover, a:active {
|
||||
color: #06e;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
/* Если размер окна очень маленький
|
||||
* располагаем имя и атрибуты файла
|
||||
* друг-под-другом
|
||||
*/
|
||||
/* responsive design */
|
||||
@media only screen and (max-width: 600px){
|
||||
.panel{
|
||||
width:94% !important;
|
||||
}
|
||||
/* если правая панель не помещаеться - прячем её */
|
||||
#right{
|
||||
display:none;
|
||||
}
|
||||
/* текущий файл под курсором */
|
||||
.current-file{
|
||||
background-color: rgb(49, 123, 249);
|
||||
background-color: rgba(49, 123, 249, .40);
|
||||
color:white;
|
||||
}
|
||||
/* делаем иконки под курсом белыми*/
|
||||
.current-file > .mini-icon{
|
||||
color:white;
|
||||
}
|
||||
.current-file > .text-file::before{
|
||||
color:white;
|
||||
}
|
||||
|
||||
.fm-header{
|
||||
display:none;
|
||||
}
|
||||
|
||||
/* меняем иконки на шрифтовые*/
|
||||
.mini-icon {
|
||||
color: rgb(246, 224, 124);
|
||||
color: rgba(246, 224, 124, 0.56);
|
||||
font: 60px 'Fontello';
|
||||
width: 40%;
|
||||
height: 0;
|
||||
margin-left: 0;
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 10px
|
||||
}
|
||||
.directory::before{
|
||||
content: '\1f4c1';
|
||||
}
|
||||
.text-file::before{
|
||||
color: rgb(26, 224, 124);
|
||||
color: rgba(26, 224, 124, 0.56);
|
||||
content: '\1f4c4';
|
||||
}
|
||||
.text-file{
|
||||
background-image:none;
|
||||
}
|
||||
|
||||
/* убираем заголовок*/
|
||||
.fm_header{
|
||||
display:none;
|
||||
}
|
||||
.mode,.size,.owner{
|
||||
/* располагаем элементы
|
||||
* один под другим
|
||||
*/
|
||||
display: table;
|
||||
float: none;
|
||||
width: 0;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
/* выводим заголовки рядом с полями */
|
||||
.name::before{
|
||||
content: 'name:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mode::before{
|
||||
content: 'mode:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.size::before{
|
||||
content: 'size:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.owner::before{
|
||||
content: 'owner:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.name{
|
||||
float: none;
|
||||
width:100%;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 601px) and (max-width: 785px){
|
||||
.panel{
|
||||
width:94% !important;
|
||||
}
|
||||
#right{
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width:786px) and (max-width: 1155px){
|
||||
.panel{
|
||||
width:94% !important;
|
||||
}
|
||||
/* если правая панель не помещаеться - прячем её */
|
||||
#right{
|
||||
display:none;
|
||||
}
|
||||
/*
|
||||
@import url(//fonts.googleapis.com/css?family=Droid+Sans+Mono);
|
||||
*/
|
||||
|
||||
/* символьный шрифт от гитхаба*/
|
||||
@font-face {
|
||||
font-family: "GeneralFoundicons";
|
||||
src: url('/font/general_foundicons.woff') format('woff');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* http://fontello.com/ */
|
||||
@font-face {
|
||||
font-family: 'Fontello';
|
||||
src: url("/font/fontello.eot");
|
||||
src: url("/font/fontello.eot?#iefix") format('embedded-opentype'), url("/font/fontello.woff") format('woff'), url("/font/fontello.ttf") format('truetype'), url("/font/fontello.svg#cloudcmd") format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Droid Sans Mono';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src: local('Droid Sans Mono'), local('DroidSansMono'), url('//themes.googleusercontent.com/static/fonts/droidsansmono/v4/ns-m2xQYezAtqh7ai59hJUYuTAAIFFn5GTWtryCmBQ4.woff') format('woff');
|
||||
}
|
||||
|
||||
body{
|
||||
font:16px "Droid Sans Mono";
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
.path-icon{
|
||||
position: relative;
|
||||
top: 3px;
|
||||
left: -4px;
|
||||
display: inline-block;
|
||||
/* размер иконки и позиция на png-файле*/
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
font-family:'FoundationIconsGeneralEnclosed';
|
||||
font-size:30px;
|
||||
color: #46A4C3;/*#55BF3F; green*/
|
||||
text-shadow:black 0 2px 1px;
|
||||
}
|
||||
.path-icon:hover{
|
||||
cursor:pointer;
|
||||
}
|
||||
.path-icon:active{
|
||||
position: relative;
|
||||
top: 4px;
|
||||
text-shadow:black 0 0 1px;
|
||||
}
|
||||
.icon{
|
||||
display:inline-block;
|
||||
width:16px;
|
||||
height:16px;
|
||||
margin-left:0.5%;
|
||||
/* font-family: 'GeneralFoundicons'; */
|
||||
font-family: 'Fontello';
|
||||
}
|
||||
.error::before{
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
color: rgb(222, 41, 41);
|
||||
cursor :default;
|
||||
content: '\2757';
|
||||
}
|
||||
.loading{
|
||||
position:relative;
|
||||
top:1px;
|
||||
background:url(/img/spinner.gif);
|
||||
}
|
||||
.error:hover{
|
||||
color:rgb(222, 41, 41);
|
||||
color:rgba(222, 41, 41, 0.81);
|
||||
}
|
||||
.refresh-icon{
|
||||
background:url(/img/panel_refresh.png) no-repeat;
|
||||
}
|
||||
.refresh-icon:active{
|
||||
/*background-position-y: -15px;*/
|
||||
background:url(/img/panel_refresh.png) 0 -15px no-repeat;
|
||||
}
|
||||
|
||||
.cmd-button{
|
||||
width: 10%;
|
||||
margin: 20px 2px 0 2px;
|
||||
overflow: hidden;
|
||||
color: rgb(49,123,249);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
background-color: white;
|
||||
border: 1.5px solid rgba(49,123,249,.40);
|
||||
}
|
||||
|
||||
.cmd-button:hover{
|
||||
border: 1.5px solid rgb(0,0,0);
|
||||
}
|
||||
|
||||
.cmd-button:active{
|
||||
color: white;
|
||||
background-color: rgb(49,123,249);
|
||||
}
|
||||
|
||||
.clear-cache{
|
||||
margin-right: 6px;
|
||||
margin-left: 7px;
|
||||
background:url(/img/console_clear.png) -4px -4px no-repeat;
|
||||
}
|
||||
.clear-cache:active{
|
||||
top:5px;
|
||||
}
|
||||
|
||||
.links{
|
||||
color:red;
|
||||
}
|
||||
|
||||
.mini-icon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
left: -5px;
|
||||
float: left;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 6px;
|
||||
/* отступ перед картинкой
|
||||
* для нормального отображения
|
||||
* рамки
|
||||
*/
|
||||
}
|
||||
/* уменьшаем отступ
|
||||
* между между иконкой и
|
||||
* именем файла во время
|
||||
* установления курсора
|
||||
*/
|
||||
.current-file > .mini-icon{
|
||||
left: -6px;
|
||||
}
|
||||
/* freeupex */
|
||||
.directory{
|
||||
/*list-style-image*/
|
||||
background-image:url('/img/dir.png');
|
||||
background-position: 0 0;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.text-file{
|
||||
/*list-style-image*/
|
||||
background-image:url('/img/txt.png');
|
||||
background-position: 0 0;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#fm{
|
||||
height: 90%;
|
||||
margin: 26px 26px 0 26px;
|
||||
}
|
||||
.fm-header{
|
||||
font-weight: bold;
|
||||
}
|
||||
#path{
|
||||
margin-left:1.5%;
|
||||
}
|
||||
.left, #left{
|
||||
float:left;
|
||||
}
|
||||
|
||||
/* фон файла, на котором курсор*/
|
||||
.current-file{
|
||||
border: 1.5px solid rgba(49, 123, 249, .40);
|
||||
}
|
||||
|
||||
.selected-file, .selected-file .name > a{
|
||||
color: rgb(254,159,224);
|
||||
}
|
||||
|
||||
.right, #right{
|
||||
float:right;
|
||||
}
|
||||
.panel{
|
||||
width: 46%;
|
||||
overflow-y: auto;
|
||||
border: 1.5px solid rgba(49, 123, 249, .40);
|
||||
}
|
||||
#keyspanel{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* информация о файлах и папках*/
|
||||
.name{
|
||||
float: left;
|
||||
width: 37%;
|
||||
/* если длина имени файла больше 16 символов
|
||||
* отрезаем лишнее, оставляя лишь 16,
|
||||
* и добавляем две точки и тайтл
|
||||
*/
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.size{
|
||||
float:left;
|
||||
width:16%;
|
||||
/* Ставим отступ, что бы
|
||||
* size не налазил на uid
|
||||
* (owner)
|
||||
*/
|
||||
margin-right: 27px;
|
||||
/* Ставим выравнивание,
|
||||
* что бы размер был в
|
||||
* одной ровной колонке
|
||||
*/
|
||||
text-align: right;
|
||||
}
|
||||
.mode{
|
||||
float: right;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
/* changin ul to panel for high speed parsing*/
|
||||
.panel, li{
|
||||
list-style-type:none;
|
||||
/* making cursor just arrow,
|
||||
* not text editing cursor
|
||||
*/
|
||||
cursor:default;
|
||||
}
|
||||
button{
|
||||
width:10%;
|
||||
}
|
||||
a{
|
||||
text-decoration:none;
|
||||
}
|
||||
a:hover, a:active {
|
||||
color: #06e;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
/* Если размер окна очень маленький
|
||||
* располагаем имя и атрибуты файла
|
||||
* друг-под-другом
|
||||
*/
|
||||
/* responsive design */
|
||||
@media only screen and (max-width: 600px){
|
||||
.panel{
|
||||
width:94% !important;
|
||||
}
|
||||
/* если правая панель не помещаеться - прячем её */
|
||||
#right{
|
||||
display:none;
|
||||
}
|
||||
/* текущий файл под курсором */
|
||||
.current-file{
|
||||
background-color: rgb(49, 123, 249);
|
||||
background-color: rgba(49, 123, 249, .40);
|
||||
color:white;
|
||||
}
|
||||
/* делаем иконки под курсом белыми*/
|
||||
.current-file > .mini-icon{
|
||||
color:white;
|
||||
}
|
||||
.current-file > .text-file::before{
|
||||
color:white;
|
||||
}
|
||||
|
||||
.fm-header{
|
||||
display:none;
|
||||
}
|
||||
|
||||
/* меняем иконки на шрифтовые*/
|
||||
.mini-icon {
|
||||
color: rgb(246, 224, 124);
|
||||
color: rgba(246, 224, 124, 0.56);
|
||||
font: 60px 'Fontello';
|
||||
width: 40%;
|
||||
height: 0;
|
||||
margin-left: 0;
|
||||
float: right;
|
||||
position: relative;
|
||||
top: 10px
|
||||
}
|
||||
.directory::before{
|
||||
content: '\1f4c1';
|
||||
}
|
||||
.text-file::before{
|
||||
color: rgb(26, 224, 124);
|
||||
color: rgba(26, 224, 124, 0.56);
|
||||
content: '\1f4c4';
|
||||
}
|
||||
.text-file{
|
||||
background-image:none;
|
||||
}
|
||||
|
||||
/* убираем заголовок*/
|
||||
.fm_header{
|
||||
display:none;
|
||||
}
|
||||
.mode,.size,.owner{
|
||||
/* располагаем элементы
|
||||
* один под другим
|
||||
*/
|
||||
display: table;
|
||||
float: none;
|
||||
width: 0;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
/* выводим заголовки рядом с полями */
|
||||
.name::before{
|
||||
content: 'name:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mode::before{
|
||||
content: 'mode:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.size::before{
|
||||
content: 'size:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.owner::before{
|
||||
content: 'owner:';
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.name{
|
||||
float: none;
|
||||
width:100%;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 601px) and (max-width: 785px){
|
||||
.panel{
|
||||
width:94% !important;
|
||||
}
|
||||
#right{
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width:786px) and (max-width: 1155px){
|
||||
.panel{
|
||||
width:94% !important;
|
||||
}
|
||||
/* если правая панель не помещаеться - прячем её */
|
||||
#right{
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script src=http://cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.8.1/dropbox.min.js></script>
|
||||
<script src=//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.9.2/dropbox.min.js></script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
|
|
|||
8
html/file.html
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<li draggable class>
|
||||
<span class="mini-icon {type}"></span>
|
||||
<span class=name>
|
||||
<a href="{link}" title="{name}" target="{target}" draggable=true>{name}</a>
|
||||
</span>
|
||||
<span class=size>{size}</span><span class=owner>{owner}</span>
|
||||
<span class=mode>{mode}</span>
|
||||
</li>
|
||||
|
|
@ -4,6 +4,8 @@
|
|||
<meta charset="utf-8">
|
||||
<!-- mobile first design -->
|
||||
<meta content="width=device-width,initial-scale=1" name="viewport" />
|
||||
<!-- chrome frame -->
|
||||
<meta http-equiv="X-UA-Compatible" content=" chrome=1" />
|
||||
<title>{title}</title>
|
||||
|
||||
<link rel=stylesheet href="/css/reset.css">
|
||||
|
|
|
|||
1
html/path.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<li class=path><span class="path-icon clear-cache" id=clear-cache title="clear cache (Ctrl+D)"></span><span class="path-icon refresh-icon" title="refresh (Ctrl+R)"><a href="{link}"></a></span><span>{path}</span></li>
|
||||
|
|
@ -13,6 +13,8 @@
|
|||
"server" : true,
|
||||
"socket" : true,
|
||||
"port" : 80,
|
||||
"sslPort" : 443,
|
||||
"ip" : null,
|
||||
"ssl" : false,
|
||||
"rest" : true
|
||||
}
|
||||
|
|
@ -34,6 +34,12 @@
|
|||
"data": {
|
||||
"id" : "00000000440E696F"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "FilePicker",
|
||||
"data": {
|
||||
"key" : "AACq5fTfzRY2E_Rw_4kyaz"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1138
lib/client.js
|
|
@ -1,12 +1,12 @@
|
|||
var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
||||
var CloudCommander, Util, DOM, CodeMirror;
|
||||
/* object contains editors CodeMirror */
|
||||
(function(CloudCmd, Util, DOM, CloudFunc){
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var KeyBinding = CloudCommander.KeyBinding,
|
||||
CodeMirrorEditor = {},
|
||||
CodeMirrorEditor = {},
|
||||
FM,
|
||||
CodeMirrorElement,
|
||||
CodeMirrorElement,
|
||||
CodeMirrorLoaded = false,
|
||||
/* indicator says CodeMirror still loads */
|
||||
Loading = false,
|
||||
|
|
@ -52,9 +52,11 @@ var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
|||
*/
|
||||
function initCodeMirror(pParams){
|
||||
if(!FM)
|
||||
FM = DOM.getFM();
|
||||
FM = DOM.getFM();
|
||||
|
||||
var lCSS = setCSS();
|
||||
var lCSS = setCSS(),
|
||||
lCurrent = DOM.getCurrentFile(),
|
||||
lPath = DOM.getCurrentPath( lCurrent );
|
||||
|
||||
CodeMirrorElement = DOM.anyload({
|
||||
name : 'div',
|
||||
|
|
@ -72,13 +74,18 @@ var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
|||
lineWrapping: false,
|
||||
autofocus : true,
|
||||
extraKeys: {
|
||||
//Сохранение
|
||||
/* Exit */
|
||||
'Esc': function(){
|
||||
Util.exec(pParams);
|
||||
DOM.remove(lCSS, document.head);
|
||||
},
|
||||
'Ctrl-S': function(){
|
||||
DOM.RESTfull.save(DOM.getCurrentPath(), lEditor.getValue());
|
||||
|
||||
/* Save */
|
||||
'Ctrl-S': function(){
|
||||
var lValue = lEditor.getValue();
|
||||
|
||||
DOM.setCurrentSize( lValue.length, lCurrent );
|
||||
DOM.RESTfull.save( lPath, lValue );
|
||||
}
|
||||
},
|
||||
readOnly : ReadOnly
|
||||
|
|
@ -89,7 +96,7 @@ var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
|||
* function loads CodeMirror js and css files
|
||||
*/
|
||||
function load(pCallBack){
|
||||
console.time('codemirror load');
|
||||
Util.time('codemirror load');
|
||||
var lDir = CloudCmd.LIBDIRCLIENT + 'editor/codemirror/',
|
||||
lFiles =
|
||||
[
|
||||
|
|
@ -103,7 +110,7 @@ var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
|||
];
|
||||
|
||||
DOM.anyLoadOnLoad(lFiles, function(){
|
||||
console.timeEnd('codemirror load');
|
||||
Util.timeEnd('codemirror load');
|
||||
CodeMirrorLoaded = true;
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
|
|
@ -118,33 +125,30 @@ var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
|||
* called do not call it again
|
||||
* if f4 key pressed couple times
|
||||
*/
|
||||
if(Loading)
|
||||
return;
|
||||
|
||||
/* checking is this link is to directory
|
||||
* when folder view is no need to edit data
|
||||
*/
|
||||
if ( DOM.getCurrentSize() === '<dir>' )
|
||||
ReadOnly = true;
|
||||
|
||||
Loading = true;
|
||||
|
||||
var lFalseLoading = function(){ Loading = false; };
|
||||
|
||||
setTimeout(lFalseLoading, 400);
|
||||
/* reading data from current file */
|
||||
DOM.getCurrentData({
|
||||
error : lFalseLoading,
|
||||
success : function(data){
|
||||
if( DOM.hidePanel() ){
|
||||
Util.exec(pCallBack, data);
|
||||
KeyBinding.unSet();
|
||||
if(!Loading){
|
||||
/* checking is this link is to directory
|
||||
* when folder view is no need to edit data */
|
||||
ReadOnly = DOM.isCurrentIsDir();
|
||||
|
||||
Loading = true;
|
||||
|
||||
var lFalseLoading = function(){ Loading = false; };
|
||||
|
||||
setTimeout(lFalseLoading, 400);
|
||||
|
||||
DOM.getCurrentData({
|
||||
error : lFalseLoading,
|
||||
success : function(data){
|
||||
if( DOM.hidePanel() ){
|
||||
Util.exec(pCallBack, data);
|
||||
KeyBinding.unSet();
|
||||
}
|
||||
|
||||
DOM.Images.hideLoad();
|
||||
lFalseLoading();
|
||||
}
|
||||
|
||||
DOM.Images.hideLoad();
|
||||
lFalseLoading();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -205,10 +209,10 @@ var CloudCommander, Util, DOM, CloudFunc, CodeMirror;
|
|||
};
|
||||
|
||||
/* добавляем обработчик клавишь */
|
||||
DOM.addKeyListener( lKeyListener );
|
||||
DOM.setButtonKey('f4', CodeMirrorEditor.show);
|
||||
DOM .addKeyListener( lKeyListener )
|
||||
.setButtonKey('f4', CodeMirrorEditor.show);
|
||||
};
|
||||
|
||||
CloudCmd.Editor.CodeMirror = CodeMirrorEditor;
|
||||
|
||||
})(CloudCommander, Util, DOM, CloudFunc);
|
||||
})(CloudCommander, Util, DOM);
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* script, fixes ie */
|
||||
//var Util, DOM, jQuery;
|
||||
var Util, DOM, jQuery;
|
||||
|
||||
(function(Util, DOM, $){
|
||||
'use strict';
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
(parent.scrollLeft + parent.clientWidth),
|
||||
|
||||
alignWithTop = overTop && !overBottom;
|
||||
|
||||
|
||||
if ((overTop || overBottom) && centerIfNeeded)
|
||||
parent.scrollTop =
|
||||
pElement.offsetTop -
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
parent.clientHeight / 2 -
|
||||
parentBorderTopWidth +
|
||||
pElement.clientHeight / 2;
|
||||
|
||||
|
||||
if ((overLeft || overRight) && centerIfNeeded)
|
||||
parent.scrollLeft =
|
||||
pElement.offsetLeft -
|
||||
|
|
@ -142,24 +142,34 @@
|
|||
parent.clientWidth / 2 -
|
||||
parentBorderLeftWidth +
|
||||
pElement.clientWidth / 2;
|
||||
|
||||
|
||||
if ( (overTop || overBottom || overLeft || overRight) &&
|
||||
!centerIfNeeded)
|
||||
pElement.scrollIntoView(alignWithTop);
|
||||
};
|
||||
|
||||
if(!document.body.classList){
|
||||
|
||||
DOM.isContainClass = function(pElement, pClass){
|
||||
var lRet,
|
||||
lClassName = pElement && pElement.className;
|
||||
|
||||
if(lClassName)
|
||||
lRet = lClassName.indexOf(pClass) > 0;
|
||||
|
||||
return lRet;
|
||||
};
|
||||
|
||||
DOM.addClass = function(pElement, pClass){
|
||||
var lSpaceChar = '',
|
||||
lClassName = pElement.className,
|
||||
lRet_b = true;
|
||||
var lRet,
|
||||
lClassName = pElement && pElement.className,
|
||||
lSpaceChar = lClassName ? ' ' : '';
|
||||
|
||||
if(lClassName) lSpaceChar = ' ';
|
||||
|
||||
if( lClassName.indexOf(pClass) < 0 )
|
||||
lRet = !DOM.isContainClass(pElement, pClass);
|
||||
if( lRet )
|
||||
pElement.className += lSpaceChar + pClass;
|
||||
else
|
||||
lRet_b = false;
|
||||
|
||||
return lRet;
|
||||
};
|
||||
|
||||
DOM.removeClass = function(pElement, pClass){
|
||||
|
|
@ -215,12 +225,9 @@
|
|||
return CacheAllowed;
|
||||
};
|
||||
|
||||
this.setAllowed = function(){
|
||||
CacheAllowed = true;
|
||||
};
|
||||
|
||||
this.UnSetAllowed = function(){
|
||||
CacheAllowed = false;
|
||||
this.setAllowed = function(pAllowed){
|
||||
CacheAllowed = pAllowed;
|
||||
return pAllowed;
|
||||
};
|
||||
|
||||
/** remove element */
|
||||
|
|
@ -255,6 +262,16 @@
|
|||
return lRet;
|
||||
},
|
||||
|
||||
/* get all cache from local storage */
|
||||
this.getAll = function(){
|
||||
var lRet = null;
|
||||
|
||||
if(CacheAllowed)
|
||||
lRet = Data;
|
||||
|
||||
return lRet;
|
||||
};
|
||||
|
||||
/** функция чистит весь кэш для всех каталогов*/
|
||||
this.clear = function(){
|
||||
var lRet = this;
|
||||
|
|
@ -269,5 +286,4 @@
|
|||
DOM.Cache = new Cache();
|
||||
}
|
||||
|
||||
|
||||
})(Util, DOM, jQuery);
|
||||
8569
lib/client/jquery-ui/jquery-ui-1.8.23.custom.js
vendored
10949
lib/client/jquery.js
vendored
|
|
@ -11,6 +11,7 @@ var CloudCommander, Util, DOM;
|
|||
ENTER : 13,
|
||||
ESC : 27,
|
||||
|
||||
SPACE : 32,
|
||||
PAGE_UP : 33,
|
||||
PAGE_DOWN : 34,
|
||||
END : 35,
|
||||
|
|
@ -18,7 +19,10 @@ var CloudCommander, Util, DOM;
|
|||
UP : 38,
|
||||
DOWN : 40,
|
||||
|
||||
Delete : 46,
|
||||
INSERT : 45,
|
||||
DELETE : 46,
|
||||
|
||||
A : 65,
|
||||
|
||||
D : 68,
|
||||
|
||||
|
|
@ -34,6 +38,10 @@ var CloudCommander, Util, DOM;
|
|||
F2 : 113,
|
||||
F3 : 114,
|
||||
F4 : 115,
|
||||
F5 : 116,
|
||||
F6 : 117,
|
||||
F7 : 118,
|
||||
F8 : 119,
|
||||
F10 : 121,
|
||||
|
||||
TRA : 192 /* Typewritten Reverse Apostrophe (`) */
|
||||
|
|
@ -61,43 +69,20 @@ var CloudCommander, Util, DOM;
|
|||
|
||||
var key_event = function(pEvent){
|
||||
/* получаем выдленный файл*/
|
||||
var lCurrentFile = DOM.getCurrentFile(), i;
|
||||
var i, n, lCurrent = DOM.getCurrentFile(),
|
||||
lKeyCode = pEvent.keyCode,
|
||||
lShift = pEvent.shiftKey,
|
||||
lAlt = pEvent.altKey,
|
||||
lCtrl = pEvent.ctrlKey;
|
||||
/* если клавиши можно обрабатывать*/
|
||||
if(keyBinded && pEvent){
|
||||
var lKeyCode = pEvent.keyCode;
|
||||
|
||||
/* open configuration window */
|
||||
if(lKeyCode === KEY.O && pEvent.altKey){
|
||||
console.log('openning config window...');
|
||||
|
||||
DOM.Images.showLoad({top: true});
|
||||
|
||||
Util.exec(CloudCmd.Config);
|
||||
}
|
||||
|
||||
else if(lKeyCode === KEY.G && pEvent.altKey)
|
||||
Util.exec(CloudCmd.GitHub);
|
||||
|
||||
else if(lKeyCode === KEY.D && pEvent.altKey){
|
||||
Util.exec(CloudCmd.DropBox);
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
/* если нажали таб:
|
||||
* переносим курсор на
|
||||
* правую панель, если
|
||||
* мы были на левой и
|
||||
* наоборот
|
||||
*/
|
||||
else if(lKeyCode === KEY.TAB){
|
||||
console.log('Tab pressed');
|
||||
|
||||
Util.tryCatchLog(function(){
|
||||
if(keyBinded){
|
||||
switch(lKeyCode){
|
||||
case KEY.TAB:
|
||||
/* changing parent panel of curent-file */
|
||||
var lPanel = DOM.getPanel(),
|
||||
lId = lPanel.id;
|
||||
|
||||
lTabPanel[lId] = lCurrentFile;
|
||||
lTabPanel[lId] = lCurrent;
|
||||
|
||||
lPanel = DOM.getPanel({active:false});
|
||||
lId = lPanel.id;
|
||||
|
|
@ -109,194 +94,253 @@ var CloudCommander, Util, DOM;
|
|||
|
||||
DOM.setCurrentFile(lFirstFileOnList);
|
||||
}
|
||||
});
|
||||
|
||||
DOM.preventDefault(pEvent);//запрет на дальнейшее действие
|
||||
break;
|
||||
|
||||
DOM.preventDefault(pEvent);//запрет на дальнейшее действие
|
||||
}
|
||||
else if(lKeyCode === KEY.Delete){
|
||||
if(pEvent.shiftKey)
|
||||
DOM.deleteCurrent(lCurrentFile);
|
||||
else
|
||||
DOM.promptDeleteCurrent(lCurrentFile);
|
||||
}
|
||||
else if(lKeyCode === KEY.F1){
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
else if(lKeyCode === KEY.F2)
|
||||
DOM.renameCurrentFile(lCurrentFile);
|
||||
/* if f3 or shift+f3 or alt+f3 pressed */
|
||||
else if(lKeyCode === KEY.F3){
|
||||
var lEditor = CloudCmd[pEvent.shiftKey ?
|
||||
'Viewer' : 'Editor'];
|
||||
case KEY.INSERT:
|
||||
DOM.setSelectedFile( lCurrent );
|
||||
DOM.setCurrentFile( lCurrent.nextSibling );
|
||||
break;
|
||||
|
||||
case KEY.DELETE:
|
||||
if(lShift){
|
||||
var lUrl = DOM.getCurrentPath(lCurrent);
|
||||
|
||||
if( DOM.isCurrentIsDir(lCurrent) )
|
||||
lUrl += '?dir';
|
||||
|
||||
DOM.RESTfull.delete(lUrl, function(){
|
||||
DOM.deleteCurrent(lCurrent);
|
||||
});
|
||||
}
|
||||
else
|
||||
DOM.promptDeleteSelected(lCurrent);
|
||||
break;
|
||||
|
||||
case KEY.F1:
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
case KEY.F2:
|
||||
DOM.renameCurrent(lCurrent);
|
||||
break;
|
||||
|
||||
case KEY.F3:
|
||||
var lEditor = CloudCmd[lShift ? 'Viewer' : 'Editor'];
|
||||
|
||||
Util.exec(lEditor, true);
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
case KEY.F4:
|
||||
DOM.Images.showLoad();
|
||||
Util.exec(CloudCmd.Editor);
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
case KEY.F5:
|
||||
DOM.copyCurrent(lCurrent);
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
case KEY.F6:
|
||||
DOM.moveCurrent(lCurrent);
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
case KEY.F7:
|
||||
DOM.promptNewDir();
|
||||
break;
|
||||
|
||||
case KEY.F8:
|
||||
DOM.promptDeleteSelected(lCurrent);
|
||||
break;
|
||||
|
||||
case KEY.F:
|
||||
DOM.promptDeleteCurrent(lCurrent);
|
||||
break;
|
||||
|
||||
case KEY.F10:
|
||||
if(lShift){
|
||||
Util.exec(CloudCmd.Menu);
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
break;
|
||||
|
||||
case KEY.TRA:
|
||||
DOM.Images.showLoad({top: true});
|
||||
Util.exec(CloudCmd.Terminal);
|
||||
break;
|
||||
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
/* if f4 pressed */
|
||||
else if(lKeyCode === KEY.F4) {
|
||||
DOM.Images.showLoad();
|
||||
|
||||
Util.exec(CloudCmd.Editor);
|
||||
case KEY.SPACE:
|
||||
var lSelected = DOM.isSelected(lCurrent),
|
||||
lDir = DOM.isCurrentIsDir(lCurrent),
|
||||
lName = DOM.getCurrentName(lCurrent);
|
||||
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
else if(lKeyCode === KEY.F10 && pEvent.shiftKey){
|
||||
Util.exec(CloudCmd.Menu);
|
||||
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
else if (lKeyCode === KEY.TRA){
|
||||
DOM.Images.showLoad({top: true});
|
||||
Util.exec(CloudCmd.Terminal);
|
||||
}
|
||||
/* навигация по таблице файлов *
|
||||
* если нажали клавишу вверх *
|
||||
* выделяем предыдущую строку */
|
||||
else if(lKeyCode === KEY.UP){
|
||||
DOM.setCurrentFile( lCurrentFile.previousSibling );
|
||||
DOM.preventDefault( pEvent );
|
||||
}
|
||||
|
||||
/* если нажали клавишу в низ *
|
||||
* выделяем следующую строку */
|
||||
else if(lKeyCode === KEY.DOWN){
|
||||
DOM.setCurrentFile( lCurrentFile.nextSibling );
|
||||
DOM.preventDefault( pEvent );
|
||||
}
|
||||
|
||||
/* если нажали клавишу Home *
|
||||
* переходим к самому верхнему *
|
||||
* элементу */
|
||||
else if(lKeyCode === KEY.HOME){
|
||||
DOM.setCurrentFile( lCurrentFile.parentElement.firstChild );
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
/* если нажали клавишу End
|
||||
* выделяем последний элемент
|
||||
*/
|
||||
else if(lKeyCode === KEY.END){
|
||||
DOM.setCurrentFile( lCurrentFile.parentElement.lastElementChild );
|
||||
DOM.preventDefault( pEvent );
|
||||
}
|
||||
|
||||
/* если нажали клавишу page down
|
||||
* проматываем экран
|
||||
*/
|
||||
else if(lKeyCode === KEY.PAGE_DOWN){
|
||||
DOM.scrollByPages( DOM.getPanel(), 1 );
|
||||
|
||||
for(i=0; i<30; i++){
|
||||
if(!lCurrentFile.nextSibling) break;
|
||||
|
||||
lCurrentFile = lCurrentFile.nextSibling;
|
||||
}
|
||||
DOM.setCurrentFile(lCurrentFile);
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
/* если нажали клавишу page up
|
||||
* проматываем экран
|
||||
*/
|
||||
else if(lKeyCode === KEY.PAGE_UP){
|
||||
DOM.scrollByPages( DOM.getPanel(), -1 );
|
||||
|
||||
var lC = lCurrentFile,
|
||||
tryCatch = function(pCurrentFile){
|
||||
Util.tryCatch(function(){
|
||||
return pCurrentFile
|
||||
.previousSibling
|
||||
.previousSibling
|
||||
.previousSibling
|
||||
.previousSibling;
|
||||
});
|
||||
};
|
||||
|
||||
for(i = 0; i < 30; i++){
|
||||
if(!lC.previousSibling || tryCatch(lC) ) break;
|
||||
|
||||
lC = lC.previousSibling;
|
||||
}
|
||||
DOM.setCurrentFile(lC);
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
/* если нажали Enter - открываем папку*/
|
||||
else if(lKeyCode === KEY.ENTER && DOM.isCurrentIsDir())
|
||||
Util.exec(CloudCmd._loadDir());
|
||||
|
||||
/* если нажали <ctr>+r
|
||||
* обновляем страницу,
|
||||
* загружаем содержимое каталога
|
||||
* при этом данные берём всегда с
|
||||
* сервера, а не из кэша
|
||||
* (обновляем кэш)
|
||||
*/
|
||||
else if(lKeyCode === KEY.R && pEvent.ctrlKey){
|
||||
console.log('<ctrl>+r pressed\n' +
|
||||
'reloading page...\n' +
|
||||
'press <alt>+q to remove all key-handlers');
|
||||
|
||||
/* Программно нажимаем на кнопку перезагрузки
|
||||
* содержимого каталога
|
||||
*/
|
||||
var lRefreshIcon = DOM.getRefreshButton();
|
||||
if(lRefreshIcon){
|
||||
/* если нашли элемент нажимаем него
|
||||
* а если не можем - нажимаем на
|
||||
* ссылку, на которую повешен eventHandler
|
||||
* onclick
|
||||
*/
|
||||
Util.exec( DOM.getListener(lRefreshIcon) );
|
||||
if(!lDir || lName === '..')
|
||||
lSelected = true;
|
||||
|
||||
Util.ifExec(lSelected, function(){
|
||||
DOM.setSelectedFile(lCurrent);
|
||||
}, function(pCallBack){
|
||||
DOM.loadCurrentSize(pCallBack, lCurrent);
|
||||
});
|
||||
|
||||
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/* если нажали <ctrl>+d чистим кэш */
|
||||
else if(lKeyCode === KEY.D && pEvent.ctrlKey){
|
||||
Util.log('<ctrl>+d pressed\n' +
|
||||
'clearing cache...\n' +
|
||||
'press <alt>+q to remove all key-handlers');
|
||||
break;
|
||||
|
||||
/* навигация по таблице файлов *
|
||||
* если нажали клавишу вверх *
|
||||
* выделяем предыдущую строку */
|
||||
case KEY.UP:
|
||||
if(lShift)
|
||||
DOM.setSelectedFile(lCurrent);
|
||||
|
||||
DOM.Cache.clear();
|
||||
DOM.preventDefault();
|
||||
}
|
||||
DOM.setCurrentFile( lCurrent.previousSibling );
|
||||
DOM.preventDefault( pEvent );
|
||||
break;
|
||||
|
||||
/* если нажали клавишу в низ *
|
||||
* выделяем следующую строку */
|
||||
case KEY.DOWN:
|
||||
if(lShift)
|
||||
DOM.setSelectedFile(lCurrent);
|
||||
|
||||
DOM.setCurrentFile( lCurrent.nextSibling );
|
||||
DOM.preventDefault( pEvent );
|
||||
break;
|
||||
|
||||
/* если нажали клавишу Home *
|
||||
* переходим к самому верхнему *
|
||||
* элементу */
|
||||
case KEY.HOME:
|
||||
DOM.setCurrentFile( lCurrent.parentElement.firstChild );
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
/* если нажали клавишу End
|
||||
* выделяем последний элемент */
|
||||
case KEY.END:
|
||||
DOM.setCurrentFile( lCurrent.parentElement.lastElementChild );
|
||||
DOM.preventDefault( pEvent );
|
||||
break;
|
||||
|
||||
/* если нажали клавишу page down
|
||||
* проматываем экран */
|
||||
case KEY.PAGE_DOWN:
|
||||
DOM.scrollByPages( DOM.getPanel(), 1 );
|
||||
|
||||
for(i=0; i<30; i++){
|
||||
if(!lCurrent.nextSibling) break;
|
||||
|
||||
lCurrent = lCurrent.nextSibling;
|
||||
}
|
||||
DOM.setCurrentFile(lCurrent);
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
/* если нажали клавишу page up проматываем экран */
|
||||
case KEY.PAGE_UP:
|
||||
DOM.scrollByPages( DOM.getPanel(), -1 );
|
||||
|
||||
var lC = lCurrent,
|
||||
tryCatch = function(pCurrentFile){
|
||||
Util.tryCatch(function(){
|
||||
return pCurrentFile
|
||||
.previousSibling
|
||||
.previousSibling
|
||||
.previousSibling
|
||||
.previousSibling;
|
||||
});
|
||||
};
|
||||
|
||||
for(i = 0; i < 30; i++){
|
||||
if(!lC.previousSibling || tryCatch(lC) ) break;
|
||||
|
||||
lC = lC.previousSibling;
|
||||
}
|
||||
DOM.setCurrentFile(lC);
|
||||
DOM.preventDefault(pEvent);
|
||||
break;
|
||||
|
||||
/* открываем папку*/
|
||||
case KEY.ENTER:
|
||||
if( DOM.isCurrentIsDir() )
|
||||
Util.exec( CloudCmd.loadDir() );
|
||||
break;
|
||||
|
||||
case KEY.A:
|
||||
if(pEvent.ctrlKey){
|
||||
var lParent = lCurrent.parentElement,
|
||||
lNodes = lParent.childNodes;
|
||||
|
||||
/* not path and fm_header */
|
||||
for(i = 2, n = lNodes.length; i < n; i++)
|
||||
DOM.setSelectedFile( lNodes[i] );
|
||||
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
/* обновляем страницу,
|
||||
* загружаем содержимое каталога
|
||||
* при этом данные берём всегда с
|
||||
* сервера, а не из кэша
|
||||
* (обновляем кэш)*/
|
||||
case KEY.R:
|
||||
if(lCtrl){
|
||||
Util.log('<ctrl>+r pressed\n' +
|
||||
'reloading page...\n' +
|
||||
'press <alt>+q to remove all key-handlers');
|
||||
|
||||
CloudCmd.refresh();
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
break;
|
||||
|
||||
/* чистим кэш */
|
||||
case KEY.D:
|
||||
if(lCtrl){
|
||||
Util.log('<ctrl>+d pressed\n' +
|
||||
'clearing cache...\n' +
|
||||
'press <alt>+q to remove all key-handlers');
|
||||
|
||||
DOM.Cache.clear();
|
||||
DOM.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
||||
/* если нажали <alt>+q
|
||||
* убираем все обработчики
|
||||
* нажатий клавиш
|
||||
*/
|
||||
else if(lKeyCode === KEY.Q && pEvent.altKey){
|
||||
console.log('<alt>+q pressed\n' +
|
||||
'<ctrl>+r reload key-handerl - removed' +
|
||||
'<ctrl>+s clear cache key-handler - removed'+
|
||||
'press <alt>+s to to set them');
|
||||
|
||||
/* обработчик нажатий клавиш снят*/
|
||||
keyBinded = false;
|
||||
|
||||
pEvent.preventDefault();//запрет на дальнейшее действие
|
||||
}
|
||||
/* убираем все обработчики
|
||||
* нажатий клавиш */
|
||||
case KEY.Q:
|
||||
if(lAlt){
|
||||
Util.log('<alt>+q pressed\n' +
|
||||
'<ctrl>+r reload key-handerl - removed' +
|
||||
'<ctrl>+s clear cache key-handler - removed'+
|
||||
'press <alt>+s to to set them');
|
||||
|
||||
/* обработчик нажатий клавиш снят*/
|
||||
keyBinded = false;
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* если нажали <alt>+s
|
||||
* устанавливаем все обработчики
|
||||
/* устанавливаем все обработчики
|
||||
* нажатий клавиш
|
||||
*/
|
||||
else if(pEvent.keyCode === KEY.S && pEvent.altKey){
|
||||
else if(lKeyCode === KEY.S && lAlt){
|
||||
/* обрабатываем нажатия на клавиши*/
|
||||
keyBinded = true;
|
||||
|
||||
console.log('<alt>+s pressed\n' +
|
||||
Util.log('<alt>+s pressed\n' +
|
||||
'<ctrl>+r reload key-handerl - set\n' +
|
||||
'<ctrl>+s clear cache key-handler - set\n' +
|
||||
'press <alt>+q to remove them');
|
||||
|
||||
pEvent.preventDefault();//запрет на дальнейшее действие
|
||||
DOM.preventDefault(pEvent);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ var CloudCommander, Util, DOM, CloudFunc, $;
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* function get menu item object for Upload To
|
||||
*/
|
||||
|
|
@ -60,20 +61,9 @@ var CloudCommander, Util, DOM, CloudFunc, $;
|
|||
}
|
||||
}
|
||||
else if( Util.isString(pObjectName) ){
|
||||
lObj.name = pObjectName;
|
||||
|
||||
lObj.callback = function(key, opt){
|
||||
lObj = function(key, opt){
|
||||
DOM.getCurrentData(function(pParams){
|
||||
var lObject = CloudCmd[pObjectName];
|
||||
|
||||
Util.ifExec('init' in lObject,
|
||||
function(){
|
||||
CloudCmd[pObjectName].uploadFile(pParams);
|
||||
},
|
||||
|
||||
function(pCallBack){
|
||||
Util.exec(lObject, pCallBack);
|
||||
});
|
||||
CloudCmd.execFromModule(pObjectName, 'uploadFile', pParams);
|
||||
});
|
||||
|
||||
Util.log('Uploading to ' + pObjectName+ '...');
|
||||
|
|
@ -91,11 +81,15 @@ var CloudCommander, Util, DOM, CloudFunc, $;
|
|||
name : pName
|
||||
};
|
||||
|
||||
if(Util.isFunction(pCallBack) )
|
||||
lRet.callback = pCallBack;
|
||||
if( Util.isFunction(pCallBack) )
|
||||
lRet.callback = pCallBack;
|
||||
|
||||
else if (Util.isObject(pCallBack))
|
||||
lRet.items = pCallBack;
|
||||
else if ( Util.isObject(pCallBack) ){
|
||||
if(pCallBack.name)
|
||||
lRet.items = pCallBack;
|
||||
else
|
||||
lRet.items = getAllItems(pCallBack);
|
||||
}
|
||||
|
||||
return lRet;
|
||||
}
|
||||
|
|
@ -162,9 +156,9 @@ var CloudCommander, Util, DOM, CloudFunc, $;
|
|||
'View' : Util.retExec(showEditor, true),
|
||||
'Edit' : Util.retExec(showEditor, false),
|
||||
'Rename' : function(){
|
||||
setTimeout( Util.retExec(DOM.renameCurrentFile), 100);
|
||||
setTimeout( Util.retExec(DOM.renameCurrent), 100);
|
||||
},
|
||||
'Delete' : Util.retExec(DOM.promptDeleteCurrent),
|
||||
'Delete' : Util.retExec(DOM.promptDeleteCurrent)
|
||||
};
|
||||
|
||||
if(UploadToItemNames.length)
|
||||
|
|
@ -172,6 +166,19 @@ var CloudCommander, Util, DOM, CloudFunc, $;
|
|||
|
||||
lMenuItems.Download = Util.retExec(downloadFromMenu);
|
||||
|
||||
lMenuItems.New = {
|
||||
'File' : DOM.promptNewFile,
|
||||
'Dir' : DOM.promptNewDir,
|
||||
|
||||
'From cloud...' : function(){
|
||||
CloudCmd.execFromModule('FilePicker', 'saveFile', function(pName, pData){
|
||||
var lPath = DOM.getCurrentDirPath() + pName;
|
||||
|
||||
DOM.RESTfull.save(lPath, pData, CloudCmd.refresh);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
lRet = {
|
||||
// define which elements trigger this menu
|
||||
selector: 'li',
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
"name": "jQuery contextMenu",
|
||||
"version": "git-master",
|
||||
"title": "jQuery.contextMenu()",
|
||||
"author": {
|
||||
"name": "Rodney Rehm",
|
||||
"url": "http://rodneyrehm.de"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "http://www.opensource.org/licenses/mit-license"
|
||||
},
|
||||
{
|
||||
"type": "GPL",
|
||||
"url": "http://opensource.org/licenses/GPL-3.0"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": ">=1.7.0"
|
||||
},
|
||||
"description": "Management facility for context menus. Developed for a large number of triggering objects. HTML5 Polyfill",
|
||||
"keywords": [
|
||||
"contextmenu",
|
||||
"context menu",
|
||||
"context-menu",
|
||||
"menu",
|
||||
"html5 menu",
|
||||
"html5 contextmenu",
|
||||
"html5 context menu",
|
||||
"html5 context-menu"
|
||||
],
|
||||
"homepage": "http://medialize.github.com/jQuery-contextMenu/",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Addy Osmani",
|
||||
"url": "https://github.com/addyosmani"
|
||||
},
|
||||
{
|
||||
"name": "Christiaan Baartse",
|
||||
"url": "https://github.com/christiaan"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"src/jquery.contextMenu.js",
|
||||
"src/jquery.contextMenu.css",
|
||||
"src/jquery.ui.position.js"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,67 +1,67 @@
|
|||
/* module make possible connectoin thrue socket.io on a client */
|
||||
var CloudCommander, DOM, Util, io;
|
||||
(function(CloudCmd, DOM, Util){
|
||||
var CloudCommander, Util, DOM, io;
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var Messages = [],
|
||||
socket,
|
||||
JqueryTerminal;
|
||||
Terminal,
|
||||
|
||||
function getJqueryTerminal(){
|
||||
ERROR_MSG = 'could not connect to socket.io\n'+
|
||||
'npm i socket.io';
|
||||
|
||||
function getTerminal(){
|
||||
return CloudCmd.Terminal.JqueryTerminal;
|
||||
}
|
||||
|
||||
DOM.jsload('/socket.io/lib/socket.io.js', {
|
||||
onload : function(){
|
||||
socket = io.connect(document.location.hostname);
|
||||
onerror : Util.retExec(Util.log, ERROR_MSG),
|
||||
|
||||
onload : function(){
|
||||
socket = io.connect(CloudCmd.HOST);
|
||||
|
||||
CloudCmd.Socket = socket;
|
||||
|
||||
socket.on('connect', function () {
|
||||
JqueryTerminal = getJqueryTerminal();
|
||||
Terminal = getTerminal();
|
||||
|
||||
if(JqueryTerminal){
|
||||
if(Terminal){
|
||||
outToTerminal({stdout: 'socket connected'});
|
||||
|
||||
JqueryTerminal.Term.resume();
|
||||
Terminal.Term.resume();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('message', function (msg) {
|
||||
var lMsg = JSON.parse(msg);
|
||||
var lMsg = Util.parseJSON(msg);
|
||||
|
||||
outToTerminal(lMsg);
|
||||
|
||||
});
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
JqueryTerminal = getJqueryTerminal();
|
||||
Terminal = getTerminal();
|
||||
|
||||
if(JqueryTerminal){
|
||||
if(Terminal){
|
||||
outToTerminal({stderr: 'socket disconected'});
|
||||
|
||||
JqueryTerminal.Term.pause();
|
||||
Terminal.Term.pause();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onerror : function(){
|
||||
console.log('could not connect to socket.io\n'+
|
||||
'npm i socket.io');
|
||||
}
|
||||
});
|
||||
|
||||
function outToTerminal(pMsg){
|
||||
var lTerm,
|
||||
lResult = true;
|
||||
var lResult, lTerm;
|
||||
|
||||
JqueryTerminal = getJqueryTerminal();
|
||||
if(JqueryTerminal)
|
||||
lTerm = JqueryTerminal.Term;
|
||||
Terminal = getTerminal();
|
||||
if(Terminal)
|
||||
lTerm = Terminal.Term;
|
||||
|
||||
if(lTerm){
|
||||
var lStdout,
|
||||
lStderr;
|
||||
|
||||
if(Messages.length){
|
||||
/* show oll msg from buffer */
|
||||
for(var i = 0, n = Messages.length; i < n; i++){
|
||||
|
|
@ -74,7 +74,7 @@ var CloudCommander, DOM, Util, io;
|
|||
if(lStderr){
|
||||
/* if it's object - convert is to string' */
|
||||
if( Util.isObject(lStderr) )
|
||||
lStderr = JSON.Stringify(lStderr);
|
||||
lStderr = Util.stringifyJSON(lStderr);
|
||||
|
||||
lTerm.error(lStderr);
|
||||
}
|
||||
|
|
@ -91,16 +91,13 @@ var CloudCommander, DOM, Util, io;
|
|||
if(lStderr && lStderr.code !== 1)
|
||||
lResult = lTerm.error(lStderr.toString());
|
||||
}
|
||||
else{
|
||||
/* if term not accesable
|
||||
* save msg to buffer
|
||||
*/
|
||||
else
|
||||
/* if term not accesable save msg to buffer */
|
||||
Messages.push(pMsg);
|
||||
lResult = false;
|
||||
}
|
||||
console.log(pMsg);
|
||||
|
||||
Util.log(pMsg);
|
||||
|
||||
return lResult;
|
||||
}
|
||||
|
||||
})(CloudCommander, DOM, Util);
|
||||
})(CloudCommander, Util, DOM);
|
||||
|
|
@ -1,111 +1,111 @@
|
|||
var CloudCommander, Util, DOM, Dropbox, cb, Client;
|
||||
/* module for work with github */
|
||||
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var DropBoxStore = {};
|
||||
|
||||
/* temporary callback function for work with github */
|
||||
cb = function (err, data){ Util.log(err || data);};
|
||||
|
||||
/* PRIVATE FUNCTIONS */
|
||||
|
||||
/**
|
||||
* function loads dropbox.js
|
||||
*/
|
||||
function load(pCallBack){
|
||||
console.time('dropbox load');
|
||||
|
||||
var lSrc = 'http://cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.8.1/dropbox.min.js',
|
||||
lLocal = CloudCmd.LIBDIRCLIENT + 'storage/dropbox/lib/dropbox.min.js',
|
||||
lOnload = function(){
|
||||
console.timeEnd('dropbox load');
|
||||
DOM.Images.hideLoad();
|
||||
|
||||
Util.exec(pCallBack);
|
||||
};
|
||||
|
||||
DOM.jsload(lSrc, {
|
||||
onload : lOnload,
|
||||
error : DOM.retJSLoad(lLocal, lOnload)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getUserData(pCallBack){
|
||||
Client.getUserInfo(function(pError, pData){
|
||||
var lHello = 'Hello ' + pData.name + ' :)!',
|
||||
lMsg = pError ? pError : lHello;
|
||||
|
||||
Util.log(lMsg);
|
||||
});
|
||||
|
||||
Util.exec(pCallBack);
|
||||
}
|
||||
/**
|
||||
* function logins on dropbox
|
||||
*
|
||||
* @param pData = {key, secret}
|
||||
*/
|
||||
DropBoxStore.login = function(pCallBack){
|
||||
CloudCmd.getModules(function(pModules){
|
||||
var lStorage = Util.findObjByNameInArr(pModules, 'storage'),
|
||||
lDropBox = Util.findObjByNameInArr(lStorage, 'DropBox'),
|
||||
lDropBoxKey = lDropBox && lDropBox.encodedKey;
|
||||
|
||||
Client = new Dropbox.Client({
|
||||
key: lDropBoxKey
|
||||
});
|
||||
|
||||
//Client.authDriver(new Dropbox.Drivers.Redirect({rememberUser: true}));
|
||||
|
||||
var lURL = CloudCmd.HOST + '/html/auth/dropbox.html';
|
||||
Client.authDriver(new Dropbox.Drivers.Popup({
|
||||
receiverUrl: lURL, noFragment: true
|
||||
}));
|
||||
|
||||
Client.authenticate(function(pError, pClient) {
|
||||
Util.log(pError);
|
||||
Client = pClient;
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
/**
|
||||
* upload file to DropBox
|
||||
*/
|
||||
DropBoxStore.uploadFile = function(pParams, pCallBack){
|
||||
var lContent = pParams.data,
|
||||
lName = pParams.name;
|
||||
|
||||
if(lContent){
|
||||
DOM.Images.showLoad();
|
||||
if(!lName)
|
||||
lName = Util.getDate();
|
||||
|
||||
Client.writeFile(lName, lContent, function(pError, pData){
|
||||
DOM.Images.hideLoad();
|
||||
Util.log(pError || pData);
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
|
||||
return lContent;
|
||||
};
|
||||
|
||||
DropBoxStore.init = function(pCallBack){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
getUserData,
|
||||
DropBoxStore.login,
|
||||
load
|
||||
]);
|
||||
|
||||
CloudCmd.DropBox.init = null;
|
||||
};
|
||||
|
||||
CloudCmd.DropBox = DropBoxStore;
|
||||
})(CloudCommander, Util, DOM);
|
||||
var CloudCommander, Util, DOM, Dropbox, cb, Client;
|
||||
/* module for work with github */
|
||||
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var DropBoxStore = {};
|
||||
|
||||
/* temporary callback function for work with github */
|
||||
cb = function (err, data){ Util.log(err || data);};
|
||||
|
||||
/* PRIVATE FUNCTIONS */
|
||||
|
||||
/**
|
||||
* function loads dropbox.js
|
||||
*/
|
||||
function load(pCallBack){
|
||||
console.time('dropbox load');
|
||||
|
||||
var lSrc = '//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.9.2/dropbox.min.js',
|
||||
lLocal = '/node_modules/dropbox/lib/dropbox.js',
|
||||
lOnload = function(){
|
||||
console.timeEnd('dropbox load');
|
||||
DOM.Images.hideLoad();
|
||||
|
||||
Util.exec(pCallBack);
|
||||
};
|
||||
|
||||
DOM.jsload(lSrc, {
|
||||
onload : lOnload,
|
||||
error : DOM.retJSLoad(lLocal, lOnload)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function getUserData(pCallBack){
|
||||
Client.getUserInfo(function(pError, pData){
|
||||
var lHello = 'Hello ' + pData.name + ' :)!',
|
||||
lMsg = pError ? pError : lHello;
|
||||
|
||||
Util.log(lMsg);
|
||||
});
|
||||
|
||||
Util.exec(pCallBack);
|
||||
}
|
||||
/**
|
||||
* function logins on dropbox
|
||||
*
|
||||
* @param pData = {key, secret}
|
||||
*/
|
||||
DropBoxStore.login = function(pCallBack){
|
||||
CloudCmd.getModules(function(pModules){
|
||||
var lStorage = Util.findObjByNameInArr(pModules, 'storage'),
|
||||
lDropBox = Util.findObjByNameInArr(lStorage, 'DropBox'),
|
||||
lDropBoxKey = lDropBox && lDropBox.encodedKey;
|
||||
|
||||
Client = new Dropbox.Client({
|
||||
key: lDropBoxKey
|
||||
});
|
||||
|
||||
//Client.authDriver(new Dropbox.Drivers.Redirect({rememberUser: true}));
|
||||
|
||||
var lURL = CloudCmd.HOST + '/html/auth/dropbox.html';
|
||||
Client.authDriver(new Dropbox.Drivers.Popup({
|
||||
receiverUrl: lURL, noFragment: true
|
||||
}));
|
||||
|
||||
Client.authenticate(function(pError, pClient) {
|
||||
Util.log(pError);
|
||||
Client = pClient;
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
/**
|
||||
* upload file to DropBox
|
||||
*/
|
||||
DropBoxStore.uploadFile = function(pParams, pCallBack){
|
||||
var lContent = pParams.data,
|
||||
lName = pParams.name;
|
||||
|
||||
if(lContent){
|
||||
DOM.Images.showLoad();
|
||||
if(!lName)
|
||||
lName = Util.getDate();
|
||||
|
||||
Client.writeFile(lName, lContent, function(pError, pData){
|
||||
DOM.Images.hideLoad();
|
||||
Util.log(pError || pData);
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
|
||||
return lContent;
|
||||
};
|
||||
|
||||
DropBoxStore.init = function(pCallBack){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
getUserData,
|
||||
DropBoxStore.login,
|
||||
load
|
||||
]);
|
||||
|
||||
CloudCmd.DropBox.init = null;
|
||||
};
|
||||
|
||||
CloudCmd.DropBox = DropBoxStore;
|
||||
})(CloudCommander, Util, DOM);
|
||||
|
|
|
|||
58
lib/client/storage/_filepicker.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
var CloudCommander, Util, DOM, $, filepicker;
|
||||
/* module for work with filepicker */
|
||||
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var FilePicker = function(){
|
||||
this.init = function(pCallBack){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
load
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
this.uploadFile = function(pParams, pCallBack){
|
||||
var lContent = pParams.data,
|
||||
lName = pParams.name;
|
||||
|
||||
filepicker.store(lContent, {filename: lName}, function(new_fpfile){
|
||||
console.log(new_fpfile);
|
||||
});
|
||||
|
||||
filepicker.pick(function(FPFile){
|
||||
console.log(FPFile.url);
|
||||
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
};
|
||||
|
||||
this.saveFile = function(pCallBack){
|
||||
filepicker.pick(function(FPFile){
|
||||
console.log(FPFile);
|
||||
|
||||
DOM.ajax({
|
||||
url : FPFile.url,
|
||||
success : function(pData){
|
||||
Util.exec(pCallBack, FPFile.filename, pData);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function load(pCallBack){
|
||||
console.time('filepicker load');
|
||||
var lHTTP = "https:"===document.location.protocol? "https:" : "http:";
|
||||
|
||||
DOM.jsload(lHTTP + '//api.filepicker.io/v1/filepicker.js', function(){
|
||||
filepicker.setKey('AACq5fTfzRY2E_Rw_4kyaz');
|
||||
DOM.Images.hideLoad();
|
||||
console.timeEnd('filepicker loaded');
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
CloudCmd.FilePicker = new FilePicker();
|
||||
})(CloudCommander, Util, DOM);
|
||||
|
|
@ -1,159 +1,159 @@
|
|||
var CloudCommander, Util, DOM, $, Github, cb;
|
||||
/* module for work with github */
|
||||
|
||||
(function(CloudCmd, Util, DOM){
|
||||
"use strict";
|
||||
|
||||
var Cache = DOM.Cache,
|
||||
GithubLocal,
|
||||
User,
|
||||
GitHubStore = {};
|
||||
|
||||
/* temporary callback function for work with github */
|
||||
cb = function (err, data){ Util.log(err || data);};
|
||||
|
||||
/* PRIVATE FUNCTIONS */
|
||||
|
||||
/**
|
||||
* function loads github.js
|
||||
*/
|
||||
function load(pCallBack){
|
||||
console.time('github load');
|
||||
|
||||
var lDir = '/lib/client/storage/github/',
|
||||
lFiles = [
|
||||
lDir + 'github.js',
|
||||
lDir + 'lib/base64.js',
|
||||
lDir + 'lib/underscore.js'
|
||||
];
|
||||
|
||||
DOM.anyLoadInParallel(lFiles, function(){
|
||||
console.timeEnd('github load');
|
||||
DOM.Images.hideLoad();
|
||||
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
GitHubStore.autorize = function(pCallBack, pCode){
|
||||
var lToken = Cache.get('token');
|
||||
if(lToken){
|
||||
GitHubStore.Login(lToken);
|
||||
Util.exec(pCallBack);
|
||||
}
|
||||
else{
|
||||
var lCode = pCode || window.location.search;
|
||||
if (lCode || Util.isContainStr(lCode, '?code=') )
|
||||
CloudCmd.getConfig(function(pConfig){
|
||||
DOM.ajax({
|
||||
type : 'put',
|
||||
url : pConfig && pConfig.api_url + '/auth',
|
||||
data : Util.removeStr(lCode, '?code='),
|
||||
success : function(pData){
|
||||
if(pData && pData.token){
|
||||
lToken = pData.token;
|
||||
|
||||
GitHubStore.Login(lToken);
|
||||
Cache.set('token', lToken);
|
||||
Util.exec(pCallBack);
|
||||
}
|
||||
else
|
||||
Util.log('Worning: token not getted...');
|
||||
}
|
||||
});
|
||||
});
|
||||
else{
|
||||
var lUrl = '//' + window.location.host + '/auth/github';
|
||||
DOM.openWindow(lUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
GitHubStore.getUserData = function(pCallBack){
|
||||
User.show(null, function(pError, pData){
|
||||
if(!pError){
|
||||
var lName = pData.name;
|
||||
Util.log('Hello ' + lName + ' :)!');
|
||||
}
|
||||
else
|
||||
DOM.Cache.remove('token');
|
||||
});
|
||||
|
||||
Util.exec(pCallBack);
|
||||
};
|
||||
|
||||
/* PUBLIC FUNCTIONS */
|
||||
GitHubStore.basicLogin = function(pUser, pPasswd){
|
||||
GithubLocal = new Github({
|
||||
username: pUser,
|
||||
password: pPasswd,
|
||||
auth : 'basic'
|
||||
});
|
||||
};
|
||||
|
||||
GitHubStore.Login = function(pToken){
|
||||
Github = GithubLocal = new Github({
|
||||
token : pToken,
|
||||
auth : 'oauth'
|
||||
});
|
||||
|
||||
User = GithubLocal.getUser();
|
||||
};
|
||||
|
||||
/**
|
||||
* function creates gist
|
||||
*/
|
||||
GitHubStore.uploadFile = function(pParams, pCallBack){
|
||||
var lContent = pParams.data,
|
||||
lName = pParams.name;
|
||||
|
||||
if(lContent){
|
||||
DOM.Images.showLoad();
|
||||
if(!lName)
|
||||
lName = Util.getDate();
|
||||
|
||||
var lGist = GithubLocal.getGist(),
|
||||
lFiles = {},
|
||||
lHost = CloudCommander.HOST,
|
||||
lOptions = {
|
||||
description: 'Uplouded by Cloud Commander from ' + lHost,
|
||||
public: true
|
||||
};
|
||||
|
||||
lFiles[lName] ={
|
||||
content: lContent
|
||||
};
|
||||
|
||||
lOptions.files = lFiles;
|
||||
|
||||
lGist.create(lOptions, function(pError, pData){
|
||||
DOM.Images.hideLoad();
|
||||
Util.log(pError || pData);
|
||||
Util.log(pData && pData.html_url);
|
||||
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
|
||||
return lContent;
|
||||
};
|
||||
|
||||
GitHubStore.init = function(pCallBack){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
GitHubStore.getUserData,
|
||||
GitHubStore.autorize,
|
||||
load
|
||||
]);
|
||||
|
||||
GitHubStore.callback = function(){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
GitHubStore.getUserData,
|
||||
]);
|
||||
};
|
||||
};
|
||||
|
||||
CloudCmd.GitHub = GitHubStore;
|
||||
})(CloudCommander, Util, DOM);
|
||||
var CloudCommander, Util, DOM, $, Github, cb;
|
||||
/* module for work with github */
|
||||
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var Cache = DOM.Cache,
|
||||
GithubLocal,
|
||||
User,
|
||||
GitHubStore = {};
|
||||
|
||||
/* temporary callback function for work with github */
|
||||
cb = function (err, data){ Util.log(err || data);};
|
||||
|
||||
/* PRIVATE FUNCTIONS */
|
||||
|
||||
/**
|
||||
* function loads github.js
|
||||
*/
|
||||
function load(pCallBack){
|
||||
console.time('github load');
|
||||
|
||||
var lDir = '/lib/client/storage/github/',
|
||||
lFiles = [
|
||||
lDir + 'github.js',
|
||||
lDir + 'lib/base64.js',
|
||||
lDir + 'lib/underscore.js'
|
||||
];
|
||||
|
||||
DOM.anyLoadInParallel(lFiles, function(){
|
||||
console.timeEnd('github load');
|
||||
DOM.Images.hideLoad();
|
||||
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
GitHubStore.autorize = function(pCallBack, pCode){
|
||||
var lToken = Cache.get('token');
|
||||
if(lToken){
|
||||
GitHubStore.Login(lToken);
|
||||
Util.exec(pCallBack);
|
||||
}
|
||||
else{
|
||||
var lCode = pCode || window.location.search;
|
||||
if (lCode || Util.isContainStr(lCode, '?code=') )
|
||||
CloudCmd.getConfig(function(pConfig){
|
||||
DOM.ajax({
|
||||
type : 'put',
|
||||
url : pConfig && pConfig.api_url + '/auth',
|
||||
data : Util.removeStr(lCode, '?code='),
|
||||
success : function(pData){
|
||||
if(pData && pData.token){
|
||||
lToken = pData.token;
|
||||
|
||||
GitHubStore.Login(lToken);
|
||||
Cache.set('token', lToken);
|
||||
Util.exec(pCallBack);
|
||||
}
|
||||
else
|
||||
Util.log('Worning: token not getted...');
|
||||
}
|
||||
});
|
||||
});
|
||||
else{
|
||||
var lUrl = '//' + window.location.host + '/auth/github';
|
||||
DOM.openWindow(lUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
GitHubStore.getUserData = function(pCallBack){
|
||||
User.show(null, function(pError, pData){
|
||||
if(!pError){
|
||||
var lName = pData.name;
|
||||
Util.log('Hello ' + lName + ' :)!');
|
||||
}
|
||||
else
|
||||
DOM.Cache.remove('token');
|
||||
});
|
||||
|
||||
Util.exec(pCallBack);
|
||||
};
|
||||
|
||||
/* PUBLIC FUNCTIONS */
|
||||
GitHubStore.basicLogin = function(pUser, pPasswd){
|
||||
GithubLocal = new Github({
|
||||
username: pUser,
|
||||
password: pPasswd,
|
||||
auth : 'basic'
|
||||
});
|
||||
};
|
||||
|
||||
GitHubStore.Login = function(pToken){
|
||||
Github = GithubLocal = new Github({
|
||||
token : pToken,
|
||||
auth : 'oauth'
|
||||
});
|
||||
|
||||
User = GithubLocal.getUser();
|
||||
};
|
||||
|
||||
/**
|
||||
* function creates gist
|
||||
*/
|
||||
GitHubStore.uploadFile = function(pParams, pCallBack){
|
||||
var lContent = pParams.data,
|
||||
lName = pParams.name;
|
||||
|
||||
if(lContent){
|
||||
DOM.Images.showLoad();
|
||||
if(!lName)
|
||||
lName = Util.getDate();
|
||||
|
||||
var lGist = GithubLocal.getGist(),
|
||||
lFiles = {},
|
||||
lHost = CloudCommander.HOST,
|
||||
lOptions = {
|
||||
description: 'Uplouded by Cloud Commander from ' + lHost,
|
||||
public: true
|
||||
};
|
||||
|
||||
lFiles[lName] ={
|
||||
content: lContent
|
||||
};
|
||||
|
||||
lOptions.files = lFiles;
|
||||
|
||||
lGist.create(lOptions, function(pError, pData){
|
||||
DOM.Images.hideLoad();
|
||||
Util.log(pError || pData);
|
||||
Util.log(pData && pData.html_url);
|
||||
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
}
|
||||
|
||||
return lContent;
|
||||
};
|
||||
|
||||
GitHubStore.init = function(pCallBack){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
GitHubStore.getUserData,
|
||||
GitHubStore.autorize,
|
||||
load
|
||||
]);
|
||||
|
||||
GitHubStore.callback = function(){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
GitHubStore.getUserData,
|
||||
]);
|
||||
};
|
||||
};
|
||||
|
||||
CloudCmd.GitHub = GitHubStore;
|
||||
})(CloudCommander, Util, DOM);
|
||||
|
|
|
|||
78
lib/client/storage/_skydrive.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
//http://isdk.dev.live.com/ISDK.aspx?category=scenarioGroup_skyDrive&index=0
|
||||
var CloudCommander, Util, DOM, WL;
|
||||
|
||||
(function(CloudCmd, Util, DOM){
|
||||
'use strict';
|
||||
|
||||
var SkyDrive = {};
|
||||
|
||||
/* PRIVATE FUNCTIONS */
|
||||
|
||||
/**
|
||||
* load google api library
|
||||
*/
|
||||
function load(pCallBack){
|
||||
console.time('SkyDrive');
|
||||
var lUrl = '//js.live.net/v5.0/wl.js';
|
||||
|
||||
DOM.jsload(lUrl, function(){
|
||||
console.timeEnd('SkyDrive load');
|
||||
DOM.Images.hideLoad();
|
||||
|
||||
Util.exec(pCallBack);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function auth(pCallBack){
|
||||
CloudCmd.getModules(function(pModules){
|
||||
var lStorage = Util.findObjByNameInArr(pModules, 'storage'),
|
||||
lSkyDrive = Util.findObjByNameInArr(lStorage, 'SkyDrive'),
|
||||
lSkyDriveKey = lSkyDrive && lSkyDrive.id;
|
||||
|
||||
WL.init({
|
||||
client_id: lSkyDriveKey,
|
||||
redirect_uri: CloudCmd.HOST,
|
||||
});
|
||||
|
||||
WL.login({
|
||||
scope: ["wl.skydrive wl.signin"]
|
||||
}).then(
|
||||
function(response) {
|
||||
Util.log(response);
|
||||
},
|
||||
function(response) {
|
||||
Util.log("Failed to authenticate.");
|
||||
});
|
||||
|
||||
WL.Event.subscribe("auth.login", onLogin);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function onLogin(session) {
|
||||
var strGreeting = "";
|
||||
WL.api({
|
||||
path: "me",
|
||||
method: "GET"
|
||||
},
|
||||
|
||||
function (response) {
|
||||
if (!response.error) {
|
||||
strGreeting = "Hi, " + response.first_name + "!";
|
||||
Util.log(strGreeting);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SkyDrive.init = function(pCallBack){
|
||||
Util.loadOnLoad([
|
||||
Util.retExec(pCallBack),
|
||||
auth,
|
||||
load
|
||||
]);
|
||||
};
|
||||
|
||||
CloudCmd.SkyDrive = SkyDrive;
|
||||
|
||||
})(CloudCommander, Util, DOM);
|
||||
37
lib/client/storage/dropbox/.gitignore
vendored
|
|
@ -1,37 +0,0 @@
|
|||
# Vim.
|
||||
*.swp
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Npm modules.
|
||||
node_modules
|
||||
|
||||
# Vendored javascript modules.
|
||||
test/vendor
|
||||
|
||||
# Build output.
|
||||
lib/dropbox.js
|
||||
lib/dropbox.min.js
|
||||
test/chrome_app/lib
|
||||
test/chrome_app/manifest.json
|
||||
test/chrome_app/node_modules
|
||||
test/chrome_app/test
|
||||
test/chrome_extension/*.js
|
||||
test/chrome_profile
|
||||
test/js
|
||||
tmp/*.js
|
||||
|
||||
# Documentation output.
|
||||
doc/*.html
|
||||
doc/assets
|
||||
doc/classes
|
||||
doc/files
|
||||
|
||||
# Node packaging output.
|
||||
dropbox-*.tgz
|
||||
|
||||
# Potentially sensitive credentials and keys used during testing.
|
||||
test/.token
|
||||
test/ssl
|
||||
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# Vim.
|
||||
*.swp
|
||||
.vimrc
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Git.
|
||||
.git
|
||||
|
||||
# Npm modules.
|
||||
node_modules
|
||||
|
||||
# Vendored javascript modules.
|
||||
test/vendor
|
||||
|
||||
# Minified library.
|
||||
lib/dropbox.min.js
|
||||
|
||||
# Sample apps.
|
||||
samples
|
||||
|
||||
# Test build output.
|
||||
test/chrome_profile
|
||||
test/js
|
||||
tmp/*.js
|
||||
|
||||
# Test code that is not related to node.js.
|
||||
test/chrome_app
|
||||
|
||||
# Documentation output.
|
||||
doc/*.html
|
||||
doc/assets
|
||||
doc/classes
|
||||
doc/files
|
||||
|
||||
# Test app logos.
|
||||
test/app_icon
|
||||
# Test automating Chrome extension.
|
||||
test/chrome_extension
|
||||
|
||||
# Potentially sensitive credentials and keys used during testing.
|
||||
test/.token
|
||||
test/ssl
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
" Indentation settings for the project: 2-space indentation, no tabs.
|
||||
set tabstop=2
|
||||
set softtabstop=2
|
||||
set shiftwidth=2
|
||||
set expandtab
|
||||
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
async = require 'async'
|
||||
{spawn, exec} = require 'child_process'
|
||||
fs = require 'fs'
|
||||
glob = require 'glob'
|
||||
log = console.log
|
||||
path = require 'path'
|
||||
remove = require 'remove'
|
||||
|
||||
# Node 0.6 compatibility hack.
|
||||
unless fs.existsSync
|
||||
fs.existsSync = (filePath) -> path.existsSync filePath
|
||||
|
||||
|
||||
task 'build', ->
|
||||
build()
|
||||
|
||||
task 'test', ->
|
||||
vendor ->
|
||||
build ->
|
||||
ssl_cert ->
|
||||
tokens ->
|
||||
run 'node_modules/mocha/bin/mocha --colors --slow 200 ' +
|
||||
'--timeout 10000 --require test/js/helper.js test/js/*test.js'
|
||||
|
||||
task 'webtest', ->
|
||||
vendor ->
|
||||
build ->
|
||||
ssl_cert ->
|
||||
tokens ->
|
||||
webtest()
|
||||
|
||||
task 'cert', ->
|
||||
remove.removeSync 'test/ssl', ignoreMissing: true
|
||||
ssl_cert()
|
||||
|
||||
task 'vendor', ->
|
||||
remove.removeSync './test/vendor', ignoreMissing: true
|
||||
vendor()
|
||||
|
||||
task 'tokens', ->
|
||||
remove.removeSync './test/.token', ignoreMissing: true
|
||||
build ->
|
||||
tokens ->
|
||||
process.exit 0
|
||||
|
||||
task 'doc', ->
|
||||
run 'node_modules/codo/bin/codo src'
|
||||
|
||||
task 'extension', ->
|
||||
run 'node_modules/coffee-script/bin/coffee ' +
|
||||
'--compile test/chrome_extension/*.coffee'
|
||||
|
||||
task 'chrome', ->
|
||||
vendor ->
|
||||
build ->
|
||||
# The v2 Chrome App API isn't supported yet.
|
||||
buildChromeApp 'app_v1'
|
||||
|
||||
task 'chrometest', ->
|
||||
vendor ->
|
||||
build ->
|
||||
# The v2 Chrome App API isn't supported yet.
|
||||
buildChromeApp 'app_v1', ->
|
||||
testChromeApp()
|
||||
|
||||
build = (callback) ->
|
||||
commands = []
|
||||
|
||||
# Ignoring ".coffee" when sorting.
|
||||
# We want "driver.coffee" to sort before "driver-browser.coffee"
|
||||
source_files = glob.sync 'src/*.coffee'
|
||||
source_files.sort (a, b) ->
|
||||
a.replace(/\.coffee$/, '').localeCompare b.replace(/\.coffee$/, '')
|
||||
|
||||
# Compile without --join for decent error messages.
|
||||
commands.push 'node_modules/coffee-script/bin/coffee --output tmp ' +
|
||||
"--compile #{source_files.join(' ')}"
|
||||
commands.push 'node_modules/coffee-script/bin/coffee --output lib ' +
|
||||
"--compile --join dropbox.js #{source_files.join(' ')}"
|
||||
# Minify the javascript, for browser distribution.
|
||||
commands.push 'node_modules/uglify-js/bin/uglifyjs --compress --mangle ' +
|
||||
'--output lib/dropbox.min.js lib/dropbox.js'
|
||||
commands.push 'node_modules/coffee-script/bin/coffee --output test/js ' +
|
||||
'--compile test/src/*.coffee'
|
||||
async.forEachSeries commands, run, ->
|
||||
callback() if callback
|
||||
|
||||
webtest = (callback) ->
|
||||
webFileServer = require './test/js/web_file_server.js'
|
||||
if 'BROWSER' of process.env
|
||||
if process.env['BROWSER'] is 'false'
|
||||
url = webFileServer.testUrl()
|
||||
console.log "Please open the URL below in your browser:\n #{url}"
|
||||
else
|
||||
webFileServer.openBrowser process.env['BROWSER']
|
||||
else
|
||||
webFileServer.openBrowser()
|
||||
callback() if callback?
|
||||
|
||||
ssl_cert = (callback) ->
|
||||
fs.mkdirSync 'test/ssl' unless fs.existsSync 'test/ssl'
|
||||
if fs.existsSync 'test/ssl/cert.pem'
|
||||
callback() if callback?
|
||||
return
|
||||
|
||||
run 'openssl req -new -x509 -days 365 -nodes -batch ' +
|
||||
'-out test/ssl/cert.pem -keyout test/ssl/cert.pem ' +
|
||||
'-subj /O=dropbox.js/OU=Testing/CN=localhost ', callback
|
||||
|
||||
vendor = (callback) ->
|
||||
# All the files will be dumped here.
|
||||
fs.mkdirSync 'test/vendor' unless fs.existsSync 'test/vendor'
|
||||
|
||||
# Embed the binary test image into a 7-bit ASCII JavaScript.
|
||||
bytes = fs.readFileSync 'test/binary/dropbox.png'
|
||||
fragments = []
|
||||
for i in [0...bytes.length]
|
||||
fragment = bytes.readUInt8(i).toString 16
|
||||
while fragment.length < 4
|
||||
fragment = '0' + fragment
|
||||
fragments.push "\\u#{fragment}"
|
||||
js = "window.testImageBytes = \"#{fragments.join('')}\";"
|
||||
fs.writeFileSync 'test/vendor/favicon.js', js
|
||||
|
||||
downloads = [
|
||||
# chai.js ships different builds for browsers vs node.js
|
||||
['http://chaijs.com/chai.js', 'test/vendor/chai.js'],
|
||||
# sinon.js also ships special builds for browsers
|
||||
['http://sinonjs.org/releases/sinon.js', 'test/vendor/sinon.js'],
|
||||
# ... and sinon.js ships an IE-only module
|
||||
['http://sinonjs.org/releases/sinon-ie.js', 'test/vendor/sinon-ie.js']
|
||||
]
|
||||
async.forEachSeries downloads, download, ->
|
||||
callback() if callback
|
||||
|
||||
testChromeApp = (callback) ->
|
||||
# Clean up the profile.
|
||||
fs.mkdirSync 'test/chrome_profile' unless fs.existsSync 'test/chrome_profile'
|
||||
|
||||
command = "\"#{chromeCommand()}\" --load-extension=test/chrome_app " +
|
||||
'--user-data-dir=test/chrome_profile --no-default-browser-check ' +
|
||||
'--no-first-run --no-service-autorun --disable-default-apps ' +
|
||||
'--homepage=about:blank --v=-1'
|
||||
|
||||
run command, ->
|
||||
callback() if callback
|
||||
|
||||
buildChromeApp = (manifestFile, callback) ->
|
||||
unless fs.existsSync 'test/chrome_app/test'
|
||||
fs.mkdirSync 'test/chrome_app/test'
|
||||
unless fs.existsSync 'test/chrome_app/node_modules'
|
||||
fs.mkdirSync 'test/chrome_app/node_modules'
|
||||
|
||||
links = [
|
||||
['lib', 'test/chrome_app/lib'],
|
||||
['node_modules/mocha', 'test/chrome_app/node_modules/mocha'],
|
||||
['node_modules/sinon-chai', 'test/chrome_app/node_modules/sinon-chai'],
|
||||
['test/.token', 'test/chrome_app/test/.token'],
|
||||
['test/binary', 'test/chrome_app/test/binary'],
|
||||
['test/html', 'test/chrome_app/test/html'],
|
||||
['test/js', 'test/chrome_app/test/js'],
|
||||
['test/vendor', 'test/chrome_app/test/vendor'],
|
||||
]
|
||||
commands = [
|
||||
"cp test/chrome_app/manifests/#{manifestFile}.json " +
|
||||
'test/chrome_app/manifest.json'
|
||||
]
|
||||
for link in links
|
||||
# fs.symlinkSync(path.resolve(link[0]), link[1]) unless fs.existsSync link[1]
|
||||
commands.push "cp -r #{link[0]} #{path.dirname(link[1])}"
|
||||
async.forEachSeries commands, run, ->
|
||||
callback() if callback
|
||||
|
||||
chromeCommand = ->
|
||||
paths = [
|
||||
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
'/Applications/Chromium.app/MacOS/Contents/Chromium',
|
||||
]
|
||||
for path in paths
|
||||
return path if fs.existsSync path
|
||||
|
||||
if 'process.platform' is 'win32'
|
||||
'chrome'
|
||||
else
|
||||
'google-chrome'
|
||||
|
||||
tokens = (callback) ->
|
||||
TokenStash = require './test/js/token_stash.js'
|
||||
tokenStash = new TokenStash
|
||||
(new TokenStash()).get ->
|
||||
callback() if callback?
|
||||
|
||||
run = (args...) ->
|
||||
for a in args
|
||||
switch typeof a
|
||||
when 'string' then command = a
|
||||
when 'object'
|
||||
if a instanceof Array then params = a
|
||||
else options = a
|
||||
when 'function' then callback = a
|
||||
|
||||
command += ' ' + params.join ' ' if params?
|
||||
cmd = spawn '/bin/sh', ['-c', command], options
|
||||
cmd.stdout.on 'data', (data) -> process.stdout.write data
|
||||
cmd.stderr.on 'data', (data) -> process.stderr.write data
|
||||
process.on 'SIGHUP', -> cmd.kill()
|
||||
cmd.on 'exit', (code) -> callback() if callback? and code is 0
|
||||
|
||||
download = ([url, file], callback) ->
|
||||
if fs.existsSync file
|
||||
callback() if callback?
|
||||
return
|
||||
|
||||
run "curl -o #{file} #{url}", callback
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
Copyright (c) 2012 Dropbox, Inc., http://www.dropbox.com
|
||||
|
||||
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.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
# Client Library for the Dropbox API
|
||||
|
||||
This is a JavaScript client library for the Dropbox API,
|
||||
[written in CoffeeScript](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md),
|
||||
suitable for use in both modern browsers and in server-side code running under
|
||||
[node.js](http://nodejs.org/).
|
||||
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
This library is tested against the following JavaScript platforms
|
||||
|
||||
* [node.js](http://nodejs.org/) 0.6 and 0.8
|
||||
* [Chrome](https://www.google.com/chrome) 23
|
||||
* [Firefox](www.mozilla.org/firefox) 17
|
||||
* Internet Explorer 9
|
||||
|
||||
Keep in mind that the versions above are not hard requirements.
|
||||
|
||||
|
||||
## Installation and Usage
|
||||
|
||||
The
|
||||
[getting started guide](https://github.com/dropbox/dropbox-js/blob/master/doc/getting_started.md)
|
||||
will help you get your first dropbox.js application up and running.
|
||||
|
||||
Peruse the source code of the
|
||||
[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples),
|
||||
and borrow as much as you need.
|
||||
|
||||
The
|
||||
[dropbox.js API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/class_index.html)
|
||||
can be a good place to bookmark while building your application.
|
||||
|
||||
If you run into a problem, take a look at
|
||||
[the dropbox.js GitHub issue list](https://github.com/dropbox/dropbox-js/issues).
|
||||
Please open a new issue if your problem wasn't already reported.
|
||||
|
||||
|
||||
## Development
|
||||
|
||||
This library is written in CoffeeScript.
|
||||
[This document](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md)
|
||||
can help you understand if that matters to you.
|
||||
|
||||
The
|
||||
[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
|
||||
will make your life easier if you need to change the source code.
|
||||
|
||||
|
||||
## Platform-Specific Issues
|
||||
|
||||
This lists the most serious problems that you might run into while using
|
||||
`dropbox.js`. See
|
||||
[the GitHub issue list](https://github.com/dropbox/dropbox-js/issues) for a
|
||||
full list of outstanding problems.
|
||||
|
||||
### node.js
|
||||
|
||||
Reading and writing binary files is currently broken.
|
||||
|
||||
### Internet Explorer 9
|
||||
|
||||
The library only works when used from `https://` pages, due to
|
||||
[these issues](http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx).
|
||||
|
||||
Reading and writing binary files is unsupported.
|
||||
|
||||
|
||||
## Copyright and License
|
||||
|
||||
The library is Copyright (c) 2012 Dropbox Inc., and distributed under the MIT
|
||||
License.
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
# Authentication Drivers
|
||||
|
||||
This document explains the structure and functionality of a `dropbox.js` OAuth
|
||||
driver, and describes the drivers that ship with the library.
|
||||
|
||||
## The OAuth Driver Interface
|
||||
|
||||
An OAuth driver is a JavaScript object that implements the methods documented
|
||||
in the
|
||||
[Dropbox.AuthDriver class](http://coffeedoc.info/github/dropbox/dropbox-js/master/classes/Dropbox/AuthDriver.html).
|
||||
This class exists solely for the purpose of documenting these methods.
|
||||
|
||||
A simple driver can get away with implementing `url` and `doAuthorize`. The
|
||||
following example shows an awfully unusable node.js driver that asks the user
|
||||
to visit the authorization URL in a browser.
|
||||
|
||||
```javascript
|
||||
var util = require("util");
|
||||
var simpleDriver = {
|
||||
url: function() { return ""; },
|
||||
doAuthorize: function(authUrl, token, tokenSecret, callback) {
|
||||
util.print("Visit the following in a browser, then press Enter\n" +
|
||||
authUrl + "\n");
|
||||
var onEnterKey = function() {
|
||||
process.stdin.removeListener("data", onEnterKey);
|
||||
callback(token);
|
||||
}
|
||||
process.stdin.addListener("data", onEnterKey);
|
||||
process.stdin.resume();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Complex drivers can take control of the OAuth process by implementing
|
||||
`onAuthStateChange`. Implementations of this method should read the `authState`
|
||||
field of the `Dropbox.Client` instance they are given to make decisions.
|
||||
Implementations should call the `credentials` and `setCredentials` methods on
|
||||
the client to control the OAuth process.
|
||||
|
||||
See the
|
||||
[Dropbox.Drivers.Redirect source](https://github.com/dropbox/dropbox-js/blob/master/src/drivers.coffee)
|
||||
for a sample implementation of `onAuthStateChange`.
|
||||
|
||||
|
||||
### The OAuth Process Steps
|
||||
|
||||
The `authenticate` method in `Dropbox.Client` implements the OAuth process as a
|
||||
finite state machine (FSM). The current state is available in the `authState`
|
||||
field.
|
||||
|
||||
The authentication FSM has the following states.
|
||||
|
||||
* `Dropbox.Client.RESET` is the initial state, where the client has no OAuth
|
||||
tokens; after `onAuthStateChange` is triggered, the client will attempt to
|
||||
obtain an OAuth request token
|
||||
* `Dropbox.Client.REQUEST` indicates that the client has obtained an OAuth
|
||||
request token; after `onAuthStateChange` is triggered, the client will call
|
||||
`doAuthorize` on the OAuth driver, to get the OAuth request token authorized by
|
||||
the user
|
||||
* `Dropbox.Client.AUTHORIZED` is reached after the `doAuthorize` calls its
|
||||
callback, indicating that the user has authorized the OAuth request token;
|
||||
after `onAuthStateChange` is triggered, the client will attempt to exchange the
|
||||
request token for an OAuth access token
|
||||
* `Dropbox.Client.DONE` indicates that the OAuth process has completed, and the
|
||||
client has an OAuth access token that can be used in API calls; after
|
||||
`onAuthStateChange` is triggered, `authorize` will call its callback function,
|
||||
and report success
|
||||
* `Dropbox.Client.SIGNED_OFF` is reached when the client's `signOut` method is
|
||||
called, after the API call succeeds; after `onAuthStateChange` is triggered,
|
||||
`signOut` will call its callback function, and report success
|
||||
* `Dropbox.Client.ERROR` is reached if any of the Dropbox API calls used by
|
||||
`authorize` or `signOut` results in an error; after `onAuthStateChange` is
|
||||
triggered, `authorize` or `signOut` will call its callback function and report
|
||||
the error
|
||||
|
||||
|
||||
## Built-in OAuth Drivers
|
||||
|
||||
`dropbox.js` ships with the OAuth drivers below.
|
||||
|
||||
### Dropbox.Drivers.Redirect
|
||||
|
||||
The recommended built-in driver for browser applications completes the OAuth
|
||||
token authorization step by redirecting the browser to the Dropbox page that
|
||||
performs the authorization and having that page redirect back to the
|
||||
application page.
|
||||
|
||||
This driver's constructor takes the following options.
|
||||
|
||||
* `useQuery` should be set to true for applications that use the URL fragment
|
||||
(the part after `#`) to store state information
|
||||
* `rememberUser` can be set to true to have the driver store the user's OAuth
|
||||
token in `localStorage`, so the user doesn't have to authorize the application
|
||||
on every request
|
||||
|
||||
Although it seems that `rememberUser` should be true by default, it brings a
|
||||
couple of drawbacks. The user's token will still be valid after signing off the
|
||||
Dropbox web site, so your application will still recognize the user and access
|
||||
their Dropbox. This behavior is unintuitive to users. A reasonable compromise
|
||||
for apps that use `rememberUser` is to provide a `Sign out` button that calls
|
||||
the `signOut` method on the app's `Dropbox.Client` instance.
|
||||
|
||||
The driver's constructor takes a `receiverPath` option t
|
||||
|
||||
[checkbox.js](https://github.com/dropbox/dropbox-js/tree/master/samples/checkbox.js)
|
||||
sample application uses `rememberUser`, and implements signing off as described
|
||||
above.
|
||||
|
||||
|
||||
### Dropbox.Drivers.Popup
|
||||
|
||||
This driver may be useful for browser applications that can't handle the
|
||||
redirections peformed by `Dropbox.Drivers.Redirect`. This driver avoids
|
||||
changing the location of the application's browser window by popping up a
|
||||
separate window, and loading the Dropbox authorization page in that window.
|
||||
|
||||
The popup method has a couple of serious drawbacks. Most browsers will not
|
||||
display the popup window by default, and instead will show a hard-to-notice
|
||||
warning that the user must interact with to display the popup. The driver's
|
||||
code for communicating between the popup and the main application window does
|
||||
not work on IE9 and below, so applications that use it will only work on
|
||||
Chrome, Firefox and IE10+.
|
||||
|
||||
If the drawbacks above are more acceptable than restructuring your application
|
||||
to handle redirects, create a page on your site that contains the
|
||||
[receiver code](https://github.com/dropbox/dropbox-js/blob/master/test/html/oauth_receiver.html),
|
||||
change the code to reflect the location of `dropbox.js` on your site, and point
|
||||
the `Dropbox.Drivers.Popup` constructor to it.
|
||||
|
||||
```javascript
|
||||
client.authDriver(new Dropbox.Drivers.Popup({
|
||||
receiverUrl: "https://url.to/oauth_receiver.html"}));
|
||||
```
|
||||
|
||||
The popup driver adds a `#` (fragment hash) to the receiver URL if necessary,
|
||||
to ensure that the user's Dropbox uid and OAuth token are passed to the
|
||||
receiver in a URL fragment. This measure may improve your users' privacy, as it
|
||||
reduces the chance that their uid or token ends up in a server log.
|
||||
|
||||
If you have a good reason to disable the behavior above, set the `noFragment`
|
||||
option to true.
|
||||
|
||||
```javascript
|
||||
client.authDriver(new Dropbox.Drivers.Popup({
|
||||
receiverUrl: "https://url.to/receiver.html", noFragment: true}));
|
||||
```
|
||||
|
||||
The popup driver implements the `rememberUser` option with the same semantics
|
||||
and caveats as the redirecting driver.
|
||||
|
||||
|
||||
### Dropbox.Drivers.Chrome
|
||||
|
||||
Google Chrome [extensions](http://developer.chrome.com/extensions/) and
|
||||
[applications](developer.chrome.com/apps/) are supported by a driver that opens
|
||||
a new browser tab (in the case of extensions and legacy applications) or
|
||||
an application window (for new applications) to complete the OAuth
|
||||
authorization.
|
||||
|
||||
To use this driver, first add the following files to your extension.
|
||||
|
||||
* the [receiver script](https://github.com/dropbox/dropbox-js/blob/master/test/src/chrome_oauth_receiver.coffee);
|
||||
the file is both valid JavaScript and valid CoffeeScript
|
||||
* the [receiver page](https://github.com/dropbox/dropbox-js/blob/master/test/html/chrome_oauth_receiver.html);
|
||||
change the page to reflect the paths to `dropbox.js` and to the receiver script
|
||||
file
|
||||
|
||||
Point the driver constructor to the receiver page:
|
||||
|
||||
```javascript
|
||||
client.authDriver(new Dropbox.Drivers.Chrome({
|
||||
receiverPath: "path/to/chrome_oauth_receiver.html"}));
|
||||
```
|
||||
|
||||
This driver caches the user's credentials so that users don't have to authorize
|
||||
applications / extensions on every browser launch. Applications and extensions'
|
||||
UI should include a method for the user to sign out of Dropbox, which can be
|
||||
implemented by calling the `signOut` instance method of `Dropbox.Client`.
|
||||
|
||||
|
||||
### Dropbox.Drivers.NodeServer
|
||||
|
||||
This driver is designed for use in the automated test suites of node.js
|
||||
applications. It completes the OAuth token authorization step by opening the
|
||||
Dropbox authorization page in a new browser window, and "catches" the OAuth
|
||||
redirection by setting up a small server using the `https` built-in node.js
|
||||
library.
|
||||
|
||||
The driver's constructor takes the following options.
|
||||
|
||||
* `port` is the HTTP port number; the default is 8192, and works well with the
|
||||
Chrome extension described below
|
||||
* `favicon` is a path to a file that will be served in response to requests to
|
||||
`/favicon.ico`; setting this to a proper image will avoid some warnings in the
|
||||
browsers' consoles
|
||||
|
||||
To fully automate your test suite, you need to load up the Chrome extension
|
||||
bundled in the `dropbox.js` source tree. The extension automatically clicks on
|
||||
the "Authorize" button in the Dropbox token authorization page, and closes the
|
||||
page after the token authorization process completes. Follow the steps in the
|
||||
[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
|
||||
to build and install the extension.
|
||||
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# dropbox.js and CoffeeScript FAQ
|
||||
|
||||
dropbox.js is written in [CoffeeScript](http://coffeescript.org/), which
|
||||
compiles into very readable JavaScript. This document addresses the concerns
|
||||
that are commonly raised by JavaScript developers that do not use or wish to
|
||||
learn CoffeeScript.
|
||||
|
||||
|
||||
## Do I need to learn CoffeeScript to use the library?
|
||||
|
||||
**No.**
|
||||
|
||||
The examples in the
|
||||
[getting started guide](https://github.com/dropbox/dropbox-js/blob/master/doc/getting_started.md)
|
||||
are all written in JavaScript. The
|
||||
[dropbox.js API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/class_index.html)
|
||||
covers the entire library, so you should not need to read the library source
|
||||
code to understand how to use it.
|
||||
|
||||
_Please open an issue if the documentation is unclear!_
|
||||
|
||||
The
|
||||
[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples),
|
||||
are written in CoffeeScript. Please use the `Try CoffeeScript` button on the
|
||||
[CoffeeScript](http://coffeescript.org/) home page to quickly compile the
|
||||
sample CoffeeScript into very readable JavaScript.
|
||||
|
||||
|
||||
## Do I need to learn CoffeeScript to know how dropbox.js works?
|
||||
|
||||
**No.**
|
||||
|
||||
You can follow the
|
||||
[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
|
||||
to build the un-minified JavaScript library in `lib/dropbox.js` and then use
|
||||
your editor's find feature to get to the source code for the methods that you
|
||||
are interested in.
|
||||
|
||||
The building instructions in the development guide do not require familiarity
|
||||
with CoffeeScript.
|
||||
|
||||
|
||||
## Do I need to learn CoffeeScript to modify dropbox.js?
|
||||
|
||||
**Yes, but you might not need to modify the library.**
|
||||
|
||||
You do need to learn CoffeeScript to change the `dropbox.js` source code. At
|
||||
the same time, you can take advantage of the library hooks and the dynamic
|
||||
nature of the JavaScript language to change the behavior of `dropbox.js`
|
||||
without touching the source code.
|
||||
|
||||
* You can implement your OAuth strategy.
|
||||
* You can add methods to the prototype classes such as `Dropbox.Client` to
|
||||
implement custom operations. _Please open an issue if you think your addition
|
||||
is generally useful!_
|
||||
* You can replace internal classes such as `Dropbox.Xhr` (or selectively
|
||||
replace methods) with wrappers that tweak the original behavior
|
||||
|
||||
|
||||
## Can I contribute to dropbox.js without learning CoffeeScript?
|
||||
|
||||
**Yes.**
|
||||
|
||||
Most of the development time is spent on API design, developing tests,
|
||||
documentation and sample code. Contributing a good testing strategy with a bug
|
||||
report can save us 90% of the development time. A feature request that also
|
||||
includes a well thought-out API change proposal and testing strategy can also
|
||||
save us 90-95% of the implementation time.
|
||||
|
||||
At the same time, _please open issues for bugs and feature requests even if you
|
||||
don't have time to include any of the above_. Knowing of a problem is the first
|
||||
step towards fixing it.
|
||||
|
||||
Last, please share your constructive suggestions on how to make `dropbox.js`
|
||||
easier to use for JavaScript developers that don't speak CoffeeScript.
|
||||
|
||||
|
||||
## Can I complain to get dropbox.js to switch away from CoffeeScript?
|
||||
|
||||
**No.**
|
||||
|
||||
At the moment, 100% of the library's development comes from unpaid, voluntary
|
||||
efforts. Switching to JavaScript would reduce the efficiency of these efforts,
|
||||
and it would kill developer motivation.
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
# dropbox.js Development
|
||||
|
||||
Read this document if you want to build `dropbox.js` or modify its source code.
|
||||
If you want to write applications using dropbox.js, check out the
|
||||
[Getting Started](getting_started.md).
|
||||
|
||||
The library is written using [CoffeeScript](http://coffeescript.org/), built
|
||||
using [cake](http://coffeescript.org/documentation/docs/cake.html), minified
|
||||
using [uglify.js](https://github.com/mishoo/UglifyJS/), tested using
|
||||
[mocha](http://visionmedia.github.com/mocha/) and
|
||||
[chai.js](http://chaijs.com/), and packaged using [npm](https://npmjs.org/).
|
||||
|
||||
If you don't "speak" CoffeeScript,
|
||||
[this document](https://github.com/dropbox/dropbox-js/blob/master/doc/coffee_faq.md)
|
||||
might address some of your concerns.
|
||||
|
||||
|
||||
## Dev Environment Setup
|
||||
|
||||
Install [node.js](http://nodejs.org/#download) to get `npm` (the node
|
||||
package manager), then use it to install the libraries required by the test
|
||||
suite.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dropbox/dropbox-js.git
|
||||
cd dropbox-js
|
||||
npm install
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Run `npm pack` and ignore any deprecation warnings that might come up.
|
||||
|
||||
```bash
|
||||
npm pack
|
||||
```
|
||||
|
||||
The build output is in the `lib/` directory. `dropbox.js` is the compiled
|
||||
library that ships in the npm package, and `dropbox.min.js` is a minified
|
||||
version, optimized for browser apps.
|
||||
|
||||
|
||||
## Test
|
||||
|
||||
Install the CoffeeScript npm package globally, so you can type `cake` instead
|
||||
of `node_modules/coffee-script/bin/cake`.
|
||||
|
||||
```bash
|
||||
npm install -g coffee-script
|
||||
```
|
||||
|
||||
First, you will need to obtain a couple of Dropbox tokens that will be used by
|
||||
the automated tests.
|
||||
|
||||
```bash
|
||||
cake tokens
|
||||
```
|
||||
|
||||
Re-run the command above if the tests fail due to authentication errors.
|
||||
|
||||
Once you have Dropbox tokens, you can run the test suite in node.js, in your
|
||||
default browser, or as a Chrome application.
|
||||
|
||||
```bash
|
||||
cake test
|
||||
cake webtest
|
||||
cake chrometest
|
||||
```
|
||||
|
||||
The library is automatically re-built when running tests, so you don't need to
|
||||
run `npm pack`. Please run the tests in both node.js and a browser before
|
||||
submitting pull requests.
|
||||
|
||||
The tests store all their data in folders named along the lines of
|
||||
`js tests.0.ac1n6lgs0e3lerk9`. If tests fail, you might have to clean up these
|
||||
folders yourself.
|
||||
|
||||
|
||||
### Solving Browser Issues
|
||||
|
||||
An easy method to test a browser in a virtual machine is to skip the automated
|
||||
browser opening.
|
||||
|
||||
```bash
|
||||
BROWSER=false cake webtest
|
||||
```
|
||||
|
||||
A similar method can be used to launch a specific browser.
|
||||
|
||||
```bash
|
||||
BROWSER=firefox cake webtest
|
||||
```
|
||||
|
||||
When fighting a bug, it can be useful to keep the server process running after
|
||||
the test suite completes, so tests can be re-started with a browser refresh.
|
||||
|
||||
```bash
|
||||
BROWSER=false NO_EXIT=1 cake webtest
|
||||
```
|
||||
|
||||
[Mocha's exclusive tests](http://visionmedia.github.com/mocha/#exclusive-tests)
|
||||
(`it.only` and `describe.only`) are very useful for quickly iterating while
|
||||
figuring out a bug.
|
||||
|
||||
|
||||
### Chrome Application / Extension Testing
|
||||
|
||||
The tests for Chrome apps / extensions require manual intervention right now.
|
||||
|
||||
The `cake chrometest` command will open a Google Chrome instance. The
|
||||
`dropbox.js Test Suite` application must be clicked.
|
||||
|
||||
|
||||
### Fully Automated Tests
|
||||
|
||||
The test suite opens up the Dropbox authorization page a few times, and also
|
||||
pops up a page that cannot close itself. dropbox.js ships with a Google Chrome
|
||||
extension that can fully automate the testing process on Chrome / Chromium.
|
||||
|
||||
The extension is written in CoffeeScript, so you will have to compile it.
|
||||
|
||||
```bash
|
||||
cake extension
|
||||
```
|
||||
|
||||
After compilation, have Chrome load the unpacked extension at
|
||||
`test/chrome_extension` and click on the scary-looking toolbar icon to activate
|
||||
the extension. The icon's color should turn red, to indicate that it is active.
|
||||
|
||||
The extension performs some checks to prevent against attacks. However, you
|
||||
should still disable the automation (by clicking on the extension icon) when
|
||||
you're not testing dropbox.js, just in case the extension code has bugs.
|
||||
|
||||
|
||||
## Release Process
|
||||
|
||||
1. At the very least, test in node.js and in a browser before releasing.
|
||||
|
||||
```bash
|
||||
cake test
|
||||
cake webtest
|
||||
```
|
||||
|
||||
1. Bump the version in `package.json`.
|
||||
|
||||
1. Publish a new npm package.
|
||||
|
||||
```bash
|
||||
npm publish
|
||||
```
|
||||
|
||||
1. Commit and tag the version bump on GitHub.
|
||||
|
||||
```bash
|
||||
git add package.json
|
||||
git commit -m "Release X.Y.Z."
|
||||
git tag -a -m "Release X.Y.Z" vX.Y.Z
|
||||
git push
|
||||
git push --tags
|
||||
```
|
||||
|
||||
1. If you haven't already, go to the
|
||||
[cdnjs GitHub page](https://github.com/cdnjs/cdnjs) and fork it.
|
||||
|
||||
1. If you haven't already, set up cdnjs on your machine.
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
git clone git@github.com:you/cdnjs.git
|
||||
cd cdnjs
|
||||
git remote add up git://github.com/cdnjs/cdnjs.git
|
||||
cd ../dropbox-js
|
||||
```
|
||||
|
||||
1. Add the new release to your cdnjs fork.
|
||||
|
||||
```bash
|
||||
cd ../cdnjs
|
||||
git checkout master
|
||||
git pull up master
|
||||
npm install
|
||||
git checkout -b dbXYZ
|
||||
mkdir ajax/libs/dropbox.js/X.Y.Z
|
||||
cp ../dropbox-js/lib/dropbox.min.js ajax/libs/dropbox.js/X.Y.Z/
|
||||
vim ajax/libs/dropbox.js/package.json # Replace "version"'s value with "X.Y.Z"
|
||||
npm test
|
||||
git add -A
|
||||
git commit -m "Added dropbox.js X.Y.Z"
|
||||
git push origin dbXYZ
|
||||
```
|
||||
|
||||
1. Go to your cdnjs for on GitHub and open a pull request. Use these examples
|
||||
of accepted
|
||||
[major release pull request](https://github.com/cdnjs/cdnjs/pull/735) and
|
||||
[minor release pull request](https://github.com/cdnjs/cdnjs/pull/753).
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
# Getting Started
|
||||
|
||||
This is a guide to writing your first dropbox.js application.
|
||||
|
||||
|
||||
## Library Setup
|
||||
|
||||
This section describes how to get the library hooked up into your application.
|
||||
|
||||
### Browser Applications
|
||||
|
||||
To get started right away, place this snippet in your page's `<head>`.
|
||||
|
||||
```html
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/dropbox.js/0.8.1/dropbox.min.js">
|
||||
</script>
|
||||
```
|
||||
|
||||
The snippet is not a typo. [cdnjs](https://cdnjs.com) recommends using
|
||||
[protocol-relative URLs](http://paulirish.com/2010/the-protocol-relative-url/).
|
||||
|
||||
To get the latest development build of dropbox.js, follow the steps in the
|
||||
[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md).
|
||||
|
||||
|
||||
#### "Powered by Dropbox" Static Web Apps
|
||||
|
||||
Before writing any source code, use the
|
||||
[console app](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/powered_by.js/public/index.html)
|
||||
to set up your Dropbox. After adding an application, place the source code at
|
||||
`/Apps/Static Web Apps/my_awesome_app/public`. You should find a pre-generated
|
||||
`index.html` file in there.
|
||||
|
||||
### node.js Applications
|
||||
|
||||
First, install the `dropbox` [npm](https://npmjs.org/) package.
|
||||
|
||||
```bash
|
||||
npm install dropbox
|
||||
```
|
||||
|
||||
Once the npm package is installed, the following `require` statement lets you
|
||||
access the same API as browser applications
|
||||
|
||||
```javascript
|
||||
var Dropbox = require("dropbox");
|
||||
```
|
||||
|
||||
|
||||
## Initialization
|
||||
|
||||
[Register your application](https://www.dropbox.com/developers/apps) to obtain
|
||||
an API key. Read the brief
|
||||
[API core concepts intro](https://www.dropbox.com/developers/start/core).
|
||||
|
||||
Once you have an API key, use it to create a `Dropbox.Client`.
|
||||
|
||||
```javascript
|
||||
var client = new Dropbox.Client({
|
||||
key: "your-key-here", secret: "your-secret-here", sandbox: true
|
||||
});
|
||||
```
|
||||
|
||||
If your application requires full Dropbox access, leave out the `sandbox: true`
|
||||
parameter.
|
||||
|
||||
|
||||
### Browser and Open-Source Applications
|
||||
|
||||
The Dropbox API guidelines ask that the API key and secret is never exposed in
|
||||
cleartext. This is an issue for browser-side and open-source applications.
|
||||
|
||||
To meet this requirement,
|
||||
[encode your API key](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/api_keys.js/public/index.html)
|
||||
and pass the encoded key string to the `Dropbox.Client` constructor.
|
||||
|
||||
```javascript
|
||||
var client = new Dropbox.Client({
|
||||
key: "encoded-key-string|it-is-really-really-long", sandbox: true
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## Authentication
|
||||
|
||||
Before you can make any API calls, you need to authenticate your application's
|
||||
user with Dropbox, and have them authorize your app's to access their Dropbox.
|
||||
|
||||
This process follows the [OAuth 1.0](http://tools.ietf.org/html/rfc5849)
|
||||
protocol, which entails sending the user to a Web page on `www.dropbox.com`,
|
||||
and then having them redirected back to your application. Each Web application
|
||||
has its requirements, so `dropbox.js` lets you customize the authentication
|
||||
process by implementing an
|
||||
[OAuth driver](https://github.com/dropbox/dropbox-js/blob/master/src/drivers.coffee).
|
||||
|
||||
At the same time, dropbox.js ships with a couple of OAuth drivers, and you
|
||||
should take advantage of them as you prototype your application.
|
||||
|
||||
Read the
|
||||
[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md)
|
||||
for further information about writing an OAuth driver, and to learn about all
|
||||
the drivers that ship with `dropbox.js`.
|
||||
|
||||
### Browser Setup
|
||||
|
||||
The following snippet will set up the recommended driver.
|
||||
|
||||
```javascript
|
||||
client.authDriver(new Dropbox.Drivers.Redirect());
|
||||
```
|
||||
|
||||
The
|
||||
[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md)
|
||||
describes some useful options that you can pass to the
|
||||
`Dropbox.Drivers.Redirect` constructor.
|
||||
|
||||
### node.js Setup
|
||||
|
||||
Single-process node.js applications should create one driver to authenticate
|
||||
all the clients.
|
||||
|
||||
```javascript
|
||||
client.authDriver(new Dropbox.Drivers.NodeServer(8191));
|
||||
```
|
||||
|
||||
The
|
||||
[authentication doc](https://github.com/dropbox/dropbox-js/blob/master/doc/auth_drivers.md)
|
||||
has useful tips on using the `NodeServer` driver.
|
||||
|
||||
### Shared Code
|
||||
|
||||
After setting up an OAuth driver, authenticating the user is one method call
|
||||
away.
|
||||
|
||||
```javascript
|
||||
client.authenticate(function(error, client) {
|
||||
if (error) {
|
||||
// Replace with a call to your own error-handling code.
|
||||
//
|
||||
// Don't forget to return from the callback, so you don't execute the code
|
||||
// that assumes everything went well.
|
||||
return showError(error);
|
||||
}
|
||||
|
||||
// Replace with a call to your own application code.
|
||||
//
|
||||
// The user authorized your app, and everything went well.
|
||||
// client is a Dropbox.Client instance that you can use to make API calls.
|
||||
doSomethingCool(client);
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handlng
|
||||
|
||||
When Dropbox API calls fail, dropbox.js methods pass a `Dropbox.ApiError`
|
||||
instance as the first parameter in their callbacks. This parameter is named
|
||||
`error` in all the code snippets on this page.
|
||||
|
||||
If `error` is a truthy value, you should either recover from the error, or
|
||||
notify the user that an error occurred. The `status` field in the
|
||||
`Dropbox.ApiError` instance contains the HTTP error code, which should be one
|
||||
of the
|
||||
[error codes in the REST API](https://www.dropbox.com/developers/reference/api#error-handling).
|
||||
|
||||
The snippet below is a template for an extensive error handler.
|
||||
|
||||
```javascript
|
||||
var showError = function(error) {
|
||||
switch (error.status) {
|
||||
case 401:
|
||||
// If you're using dropbox.js, the only cause behind this error is that
|
||||
// the user token expired.
|
||||
// Get the user through the authentication flow again.
|
||||
break;
|
||||
|
||||
case 404:
|
||||
// The file or folder you tried to access is not in the user's Dropbox.
|
||||
// Handling this error is specific to your application.
|
||||
break;
|
||||
|
||||
case 507:
|
||||
// The user is over their Dropbox quota.
|
||||
// Tell them their Dropbox is full. Refreshing the page won't help.
|
||||
break;
|
||||
|
||||
case 503:
|
||||
// Too many API requests. Tell the user to try again later.
|
||||
// Long-term, optimize your code to use fewer API calls.
|
||||
break;
|
||||
|
||||
case 400: // Bad input parameter
|
||||
case 403: // Bad OAuth request.
|
||||
case 405: // Request method not expected
|
||||
default:
|
||||
// Caused by a bug in dropbox.js, in your application, or in Dropbox.
|
||||
// Tell the user an error occurred, ask them to refresh the page.
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
`Dropbox.Client` also supports a DOM event-like API for receiving all errors.
|
||||
This can be used to log API errors, or to upload them to your server for
|
||||
further analysis.
|
||||
|
||||
```javascript
|
||||
client.onError.addListener(function(error) {
|
||||
if (window.console) { // Skip the "if" in node.js code.
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## The Fun Part
|
||||
|
||||
Authentication was the hard part of the API integration, and error handling was
|
||||
the most boring part. Now that these are both behind us, you can interact
|
||||
with the user's Dropbox and focus on coding up your application!
|
||||
|
||||
The following sections have some commonly used code snippets. The
|
||||
[Dropbox.Client API reference](http://coffeedoc.info/github/dropbox/dropbox-js/master/classes/Dropbox/Client.html)
|
||||
will help you navigate less common scenarios, and the
|
||||
[Dropbox REST API reference](https://www.dropbox.com/developers/reference/api)
|
||||
describes the underlying HTTP protocol, and can come in handy when debugging
|
||||
your application, or if you want to extend dropbox.js.
|
||||
|
||||
### User Info
|
||||
|
||||
```javascript
|
||||
client.getUserInfo(function(error, userInfo) {
|
||||
if (error) {
|
||||
return showError(error); // Something went wrong.
|
||||
}
|
||||
|
||||
alert("Hello, " + userInfo.name + "!");
|
||||
});
|
||||
```
|
||||
|
||||
### Write a File
|
||||
|
||||
```javascript
|
||||
client.writeFile("hello_world.txt", "Hello, world!\n", function(error, stat) {
|
||||
if (error) {
|
||||
return showError(error); // Something went wrong.
|
||||
}
|
||||
|
||||
alert("File saved as revision " + stat.revisionTag);
|
||||
});
|
||||
```
|
||||
|
||||
### Read a File
|
||||
|
||||
```javascript
|
||||
client.readFile("hello_world.txt", function(error, data) {
|
||||
if (error) {
|
||||
return showError(error); // Something went wrong.
|
||||
}
|
||||
|
||||
alert(data); // data has the file's contents
|
||||
});
|
||||
```
|
||||
|
||||
### List a Directory's Contents
|
||||
|
||||
```javascript
|
||||
client.readdir("/", function(error, entries) {
|
||||
if (error) {
|
||||
return showError(error); // Something went wrong.
|
||||
}
|
||||
|
||||
alert("Your Dropbox contains " + entries.join(", "));
|
||||
});
|
||||
```
|
||||
|
||||
### Sample Applications
|
||||
|
||||
Check out the
|
||||
[sample apps](https://github.com/dropbox/dropbox-js/tree/master/samples)
|
||||
to see how all these concepts play out together.
|
||||
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# dropbox.js Build Directory
|
||||
|
||||
dropbox.js is written in [CoffeeScript](http://coffeescript.org/), and its
|
||||
source code is in the `src/` directory. The `lib/` directory contains the
|
||||
compiled JavaScript produced by the build process.
|
||||
|
||||
The
|
||||
[development guide](https://github.com/dropbox/dropbox-js/blob/master/doc/development.md)
|
||||
contains a step-by-step guide to the library's build process, and will help you
|
||||
populate this directory.
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
{
|
||||
"name": "dropbox",
|
||||
"version": "0.8.1",
|
||||
"description": "Client library for the Dropbox API",
|
||||
"keywords": ["dropbox", "filesystem", "storage"],
|
||||
"homepage": "http://github.com/dropbox/dropbox-js",
|
||||
"author": "Victor Costan <victor@costan.us> (http://www.costan.us)",
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
"Aakanksha Sarda <aaki@mit.edu>"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dropbox/dropbox-js.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"open": ">= 0.0.2",
|
||||
"xmlhttprequest": ">= 1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"async": ">= 0.1.22",
|
||||
"chai": ">= 1.4.0",
|
||||
"codo": ">= 1.5.4",
|
||||
"coffee-script": ">= 1.4.0",
|
||||
"express": ">= 3.0.6",
|
||||
"glob": ">= 3.1.14",
|
||||
"mocha": ">= 1.7.4",
|
||||
"open": "https://github.com/pwnall/node-open/tarball/master",
|
||||
"remove": ">= 0.1.5",
|
||||
"sinon": ">= 1.5.2",
|
||||
"sinon-chai": ">= 2.3.0",
|
||||
"uglify-js": ">= 2.2.3"
|
||||
},
|
||||
"main": "lib/dropbox.js",
|
||||
"directories": {
|
||||
"doc": "doc",
|
||||
"lib": "lib",
|
||||
"src": "src",
|
||||
"test": "test"
|
||||
},
|
||||
"scripts": {
|
||||
"prepublish": "node_modules/coffee-script/bin/cake build",
|
||||
"test": "node_modules/coffee-script/bin/cake test"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# Checkbox, a dropbox.js Sample Application
|
||||
|
||||
This application demonstrates the use of the JavaScript client library for the
|
||||
Dropbox API to implement a Dropbox-backed To Do list application.
|
||||
|
||||
In 70 lines of HTML, and 250 lines of commented CoffeeScript, Checkbox lets you
|
||||
store your To Do list in your Dropbox! Just don't expect award winning design
|
||||
or usability from a sample application.
|
||||
|
||||
See this sample in action
|
||||
[here](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/checkbox.js/public/index.html).
|
||||
|
||||
|
||||
## Dropbox Integration
|
||||
|
||||
This proof-of-concept application uses the "App folder" Dropbox access level,
|
||||
so Dropbox automatically creates a directory for its app data in the users'
|
||||
Dropboxes. The data model optimizes for ease of development and debugging.
|
||||
Each task is stored as a file whose name is the task’s description. Tasks are
|
||||
grouped under two folders, active and done.
|
||||
|
||||
The main advantage of this data model is that operations on tasks cleanly map
|
||||
to file operations in Dropbox. At initialization time, the application creates
|
||||
its two folders, active and done. A task is created by writing an empty string
|
||||
to a file in the active folder, marked as completed by moving the file to the
|
||||
done folder, and removed by deleting the associated file.
|
||||
|
||||
The lists of tasks are obtained by listing the contents of the active and done
|
||||
folders. The data model can be easily extended, by storing JSON-encoded
|
||||
information, such as deadlines, in the task files.
|
||||
|
||||
This sample uses the following `Dropbox.Client` methods:
|
||||
|
||||
* authenticate
|
||||
* signOff
|
||||
* getUserInfo
|
||||
* mkdir
|
||||
* readdir
|
||||
* writeFile
|
||||
* move
|
||||
* remove
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
This sample does not require building. Follow the steps below to get your own
|
||||
copy of the sample that you can hack on.
|
||||
|
||||
1. [Create a powered_by.js app in your Dropbox](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/powered_by.js/public/index.html).
|
||||
1. [Get your own API key](https://www.dropbox.com/developers/apps).
|
||||
1. [Encode your API key](https://dl-web.dropbox.com/spa/pjlfdak1tmznswp/api_keys.js/public/index.html).
|
||||
1. Copy the source code to `/Apps/Static Web Apps/powered_by.js` in your Dropbox
|
||||
|
||||
## Dependencies
|
||||
|
||||
The application uses the following JavaScript libraries.
|
||||
|
||||
* [dropbox.js](https://github.com/dropbox/dropbox-js) for Dropbox integration
|
||||
* [less](http://lesscss.org/) for CSS conciseness
|
||||
* [CoffeeScript](http://coffeescript.org/) for JavaScript conciseness
|
||||
* [jQuery](http://jquery.com/) for cross-browser compatibitility
|
||||
|
||||
The icons used in the application are all from
|
||||
[the noun project](http://thenounproject.com/).
|
||||
|
||||
The application follows a good practice of packaging its dependencies, and not
|
||||
hot-linking them.
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
# vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab :
|
||||
|
||||
# Controller/View for the application.
|
||||
class Checkbox
|
||||
# @param {Dropbox.Client} dbClient a non-authenticated Dropbox client
|
||||
# @param {DOMElement} root the app's main UI element
|
||||
constructor: (@dbClient, root) ->
|
||||
@$root = $ root
|
||||
@taskTemplate = $('#task-template').text()
|
||||
@$activeList = $('#active-task-list')
|
||||
@$doneList = $('#done-task-list')
|
||||
$('#signout-button').click (event) => @onSignOut event
|
||||
|
||||
@dbClient.authenticate (error, data) =>
|
||||
return @showError(error) if error
|
||||
@dbClient.getUserInfo (error, userInfo) =>
|
||||
return @showError(error) if error
|
||||
$('#user-name').text userInfo.name
|
||||
@tasks = new Tasks @, @dbClient
|
||||
@tasks.load =>
|
||||
@wire()
|
||||
@render()
|
||||
@$root.removeClass 'hidden'
|
||||
|
||||
# Re-renders all the data.
|
||||
render: ->
|
||||
@$activeList.empty()
|
||||
@$doneList.empty()
|
||||
@renderTask(task) for task in @tasks.active
|
||||
@renderTask(task) for task in @tasks.done
|
||||
|
||||
# Renders a task into the
|
||||
renderTask: (task) ->
|
||||
$list = if task.done then @$doneList else @$activeList
|
||||
$list.append @$taskDom(task)
|
||||
|
||||
# Renders the list element representing a task.
|
||||
#
|
||||
# @param {Task} task the task to be rendered
|
||||
# @return {jQuery<li>} jQuery wrapper for the DOM representing the task
|
||||
$taskDom: (task) ->
|
||||
$task = $ @taskTemplate
|
||||
$('.task-name', $task).text task.name
|
||||
$('.task-remove-button', $task).click (event) => @onRemoveTask event, task
|
||||
if task.done
|
||||
$('.task-done-button', $task).addClass 'hidden'
|
||||
$('.task-active-button', $task).click (event) =>
|
||||
@onActiveTask event, task
|
||||
else
|
||||
$('.task-active-button', $task).addClass 'hidden'
|
||||
$('.task-done-button', $task).click (event) => @onDoneTask event, task
|
||||
$task
|
||||
|
||||
# Called when the user wants to create a new task.
|
||||
onNewTask: (event) ->
|
||||
event.preventDefault()
|
||||
name = $('#new-task-name').val()
|
||||
if @tasks.findByName name
|
||||
alert "You already have this task on your list!"
|
||||
else
|
||||
$('#new-task-button').attr 'disabled', 'disabled'
|
||||
$('#new-task-name').attr 'disabled', 'disabled'
|
||||
task = new Task()
|
||||
task.name = name
|
||||
@tasks.addTask task, =>
|
||||
$('#new-task-name').removeAttr('disabled').val ''
|
||||
$('#new-task-button').removeAttr 'disabled'
|
||||
@renderTask task
|
||||
|
||||
# Called when the user wants to mark a task as done.
|
||||
onDoneTask: (event, task) ->
|
||||
$task = @$taskElement event.target
|
||||
$('button', $task).attr 'disabled', 'disabled'
|
||||
@tasks.setTaskDone task, true, =>
|
||||
$task.remove()
|
||||
@renderTask task
|
||||
|
||||
# Called when the user wants to mark a task as active.
|
||||
onActiveTask: (event, task) ->
|
||||
$task = @$taskElement event.target
|
||||
$('button', $task).attr 'disabled', 'disabled'
|
||||
@tasks.setTaskDone task, false, =>
|
||||
$task.remove()
|
||||
@renderTask task
|
||||
|
||||
# Called when the user wants to permanently remove a task.
|
||||
onRemoveTask: (event, task) ->
|
||||
$task = @$taskElement event.target
|
||||
$('button', $task).attr 'disabled', 'disabled'
|
||||
@tasks.removeTask task, ->
|
||||
$task.remove()
|
||||
|
||||
# Called when the user wants to sign out of the application.
|
||||
onSignOut: (event, task) ->
|
||||
@dbClient.signOut (error) =>
|
||||
return @showError(error) if error
|
||||
window.location.reload()
|
||||
|
||||
# Finds the DOM element representing a task.
|
||||
#
|
||||
# @param {DOMElement} element any element inside the task element
|
||||
# @return {jQuery<DOMElement>} a jQuery wrapper around the DOM element
|
||||
# representing a task
|
||||
$taskElement: (element) ->
|
||||
$(element).closest 'li.task'
|
||||
|
||||
# Sets up listeners for the relevant DOM events.
|
||||
wire: ->
|
||||
$('#new-task-form').submit (event) => @onNewTask event
|
||||
|
||||
# Updates the UI to show that an error has occurred.
|
||||
showError: (error) ->
|
||||
$('#error-notice').removeClass 'hidden'
|
||||
console.log error if window.console
|
||||
|
||||
# Model that wraps all a user's tasks.
|
||||
class Tasks
|
||||
# @param {Checkbox} controller the application controller
|
||||
constructor: (@controller) ->
|
||||
@dbClient = @controller.dbClient
|
||||
[@active, @done] = [[], []]
|
||||
|
||||
# Reads all the from a user's Dropbox.
|
||||
#
|
||||
# @param {function()} done called when all the tasks are read from the user's
|
||||
# Dropbox, and the active and done properties are set
|
||||
load: (done) ->
|
||||
# We read the done tasks and the active tasks in parallel. The variables
|
||||
# below tell us when we're done with both.
|
||||
readActive = readDone = false
|
||||
|
||||
@dbClient.mkdir '/active', (error, stat) =>
|
||||
# Issued mkdir so we always have a directory to read from.
|
||||
# In most cases, this will fail, so don't bother checking for errors.
|
||||
@dbClient.readdir '/active', (error, entries, dir_stat, entry_stats) =>
|
||||
return @showError(error) if error
|
||||
@active = ((new Task()).fromStat(stat) for stat in entry_stats)
|
||||
readActive = true
|
||||
done() if readActive and readDone
|
||||
@dbClient.mkdir '/done', (error, stat) =>
|
||||
@dbClient.readdir '/done', (error, entries, dir_stat, entry_stats) =>
|
||||
return @showError(error) if error
|
||||
@done = ((new Task()).fromStat(stat) for stat in entry_stats)
|
||||
readDone = true
|
||||
done() if readActive and readDone
|
||||
@
|
||||
|
||||
# Adds a new task to the user's set of tasks.
|
||||
#
|
||||
# @param {Task} task the task to be added
|
||||
# @param {function()} done called when the task is saved to the user's
|
||||
# Dropbox
|
||||
addTask: (task, done) ->
|
||||
task.cleanupName()
|
||||
@dbClient.writeFile task.path(), '', (error, stat) =>
|
||||
return @showError(error) if error
|
||||
@addTaskToModel task
|
||||
done()
|
||||
|
||||
# Returns a task with the given name, if it exists.
|
||||
#
|
||||
# @param {String} name the name to search for
|
||||
# @return {?Task} task the task with the given name, or null if no such task
|
||||
# exists
|
||||
findByName: (name) ->
|
||||
for tasks in [@active, @done]
|
||||
for task in tasks
|
||||
return task if task.name is name
|
||||
null
|
||||
|
||||
# Removes a task from the list of tasks.
|
||||
#
|
||||
# @param {Task} task the task to be removed
|
||||
# @param {function()} done called when the task is removed from the user's
|
||||
# Dropbox
|
||||
removeTask: (task, done) ->
|
||||
@dbClient.remove task.path(), (error, stat) =>
|
||||
return @showError(error) if error
|
||||
@removeTaskFromModel task
|
||||
done()
|
||||
|
||||
# Marks a active task as done, or a done task as active.
|
||||
#
|
||||
# @param {Task} the task to be changed
|
||||
setTaskDone: (task, newDoneValue, done) ->
|
||||
[oldDoneValue, task.done] = [task.done, newDoneValue]
|
||||
newPath = task.path()
|
||||
task.done = oldDoneValue
|
||||
|
||||
@dbClient.move task.path(), newPath, (error, stat) =>
|
||||
return @showError(error) if error
|
||||
@removeTaskFromModel task
|
||||
task.done = newDoneValue
|
||||
@addTaskToModel task
|
||||
done()
|
||||
|
||||
# Adds a task to the in-memory model. Should not be called directly.
|
||||
addTaskToModel: (task) ->
|
||||
@taskArray(task).push task
|
||||
|
||||
# Remove a task from the in-memory model. Should not be called directly.
|
||||
removeTaskFromModel: (task) ->
|
||||
taskArray = @taskArray task
|
||||
for _task, index in taskArray
|
||||
if _task is task
|
||||
taskArray.splice index, 1
|
||||
break
|
||||
|
||||
# @param {Task} the task whose containing array should be returned
|
||||
# @return {Array<Task>} the array that should contain the given task
|
||||
taskArray: (task) ->
|
||||
if task.done then @done else @active
|
||||
|
||||
# Updates the UI to show that an error has occurred.
|
||||
showError: (error) ->
|
||||
@controller.showError error
|
||||
|
||||
# Model for a single user task.
|
||||
class Task
|
||||
# Creates a task with default values.
|
||||
constructor: ->
|
||||
@name = null
|
||||
@done = false
|
||||
|
||||
# Reads data about a task from the stat of is file in a user's Dropbox.
|
||||
#
|
||||
# @param {Dropbox.Stat} entry the directory entry representing the task
|
||||
fromStat: (entry) ->
|
||||
@name = entry.name
|
||||
@done = entry.path.split('/', 3)[1] is 'done'
|
||||
@
|
||||
|
||||
# Cleans up the task name so that it's valid Dropbox file name.
|
||||
cleanupName: (name) ->
|
||||
# English-only hack that removes slashes from the task name.
|
||||
@name = @name.replace(/\ \/\ /g, ' or ').replace(/\//g, ' or ')
|
||||
@
|
||||
|
||||
# Path to the file representing the task in the user's Dropbox.
|
||||
# @return {String} fully-qualified path
|
||||
path: ->
|
||||
(if @done then '/done/' else '/active/') + @name
|
||||
|
||||
# Start up the code when the DOM is fully loaded.
|
||||
$ ->
|
||||
client = new Dropbox.Client(
|
||||
key: '/Fahm0FLioA|ZxKxLxy5irfHqsCRs+Ceo8bwJjVPu8xZlfjgGzeCjQ', sandbox: true)
|
||||
client.authDriver new Dropbox.Drivers.Redirect(rememberUser: true)
|
||||
new Checkbox client, '#app-ui'
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
// vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab :
|
||||
|
||||
.linear-gradient (@top: #ffffff, @bottom: #000000) {
|
||||
background: @top;
|
||||
filter: ~"progid:DXImageTransform.Microsoft.gradient(startColorstr='@{top}', endColorstr='@{bottom}')";
|
||||
background: -webkit-linear-gradient(top, @top 0%, @bottom 100%);
|
||||
background: -moz-linear-gradient(top, @top 0%, @bottom 100%);
|
||||
background: -ms-linear-gradient(top, @top 0%, @bottom 100%);
|
||||
background: linear-gradient(to bottom, @top 0%, @bottom 100%);
|
||||
}
|
||||
.border-radius (@radius) {
|
||||
-webkit-border-radius: @radius;
|
||||
-moz-border-radius: @radius;
|
||||
border-radius: @radius;
|
||||
}
|
||||
.box-sizing (@sizing) {
|
||||
-webkit-box-sizing: @sizing;
|
||||
-moz-box-sizing: @sizing;
|
||||
box-sizing: @sizing;
|
||||
}
|
||||
.box-shadow (@offset-x, @offset-y, @blur, @spread, @color) {
|
||||
-webkit-box-shadow: @offset-x @offset-y @blur @spread @color;
|
||||
-moz-box-shadow: @offset-x @offset-y @blur @spread @color;
|
||||
box-shadow: @offset-x @offset-y @blur @spread @color;
|
||||
}
|
||||
|
||||
.font-verdana () {
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
}
|
||||
.font-helvetica () {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
html, body {
|
||||
.linear-gradient (#ffffff, #f2f7fc);
|
||||
background-attachment: fixed;
|
||||
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#error-notice {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
.box-sizing(border-box);
|
||||
|
||||
color: hsl(0, 75%, 33%);
|
||||
font-size: 12pt;
|
||||
.font-verdana();
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0.2em 0 0.1em 0;
|
||||
|
||||
#error-refresh-button {
|
||||
margin: 0 1em;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#app-ui {
|
||||
width: 700px;
|
||||
margin: 0 auto;
|
||||
|
||||
h1 {
|
||||
.font-verdana();
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
line-height: 2;
|
||||
text-transform: lowercase;
|
||||
color: #bfbfbf;
|
||||
margin: 0;
|
||||
padding: 2em 0 0.25em 0;
|
||||
|
||||
small {
|
||||
.font-verdana();
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
color: #888888;
|
||||
|
||||
a {
|
||||
font-style: normal;
|
||||
color: #649cd1;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#notebook-page {
|
||||
border: 1px solid #aaaaaa;
|
||||
-webkit-border-top-left-radius: 8px;
|
||||
-webkit-border-top-right-radius: 8px;
|
||||
-moz-border-radius-topleft: 8px;
|
||||
-moz-border-radius-topright: 8px;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
|
||||
.box-shadow(0, 0, 10px, 5px, rgba(0, 0, 0, 0.05));
|
||||
|
||||
background-color: #fffddb;
|
||||
margin: 0;
|
||||
padding: 24px 0 60px 0;
|
||||
}
|
||||
#notebook-page, .task {
|
||||
background-image: -webkit-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%),
|
||||
-webkit-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%);
|
||||
background-image: -moz-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%),
|
||||
-moz-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%);
|
||||
background-image: -ms-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%),
|
||||
-ms-linear-gradient(top, #f3aaaa 0%, #f3aaaa 100%);
|
||||
background-image: linear-gradient(to bottom, #f3aaaa 0%, #f3aaaa 100%),
|
||||
linear-gradient(to bottom, #f3aaaa 0%, #f3aaaa 100%);
|
||||
background-position: 70px 0px, 76px 0px;
|
||||
background-size: 1px 100%, 1px 100%;
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
}
|
||||
|
||||
#user-info {
|
||||
margin: 0;
|
||||
padding: 0 16px 8px 88px;
|
||||
|
||||
color: #555555;
|
||||
.font-helvetica();
|
||||
font-size: 16px;
|
||||
line-height: 32px;
|
||||
font-weight: 200;
|
||||
text-align: right;
|
||||
|
||||
#user-name {
|
||||
display: inline-block;
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
|
||||
#signout-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover {
|
||||
color: #000000;
|
||||
|
||||
#signout-button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#new-task-form, h2, .task, .empty-task {
|
||||
border-bottom: 1px solid #c9e4f2;
|
||||
.font-helvetica();
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
line-height: 36px;
|
||||
color: #555555;
|
||||
|
||||
margin: 0;
|
||||
padding: 6px 10px 6px 88px;
|
||||
}
|
||||
|
||||
#new-task-form {
|
||||
margin: 0;
|
||||
|
||||
#new-task-name {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
|
||||
background: rgb(255, 254, 236);
|
||||
}
|
||||
.task-actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
}
|
||||
.task-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.task {
|
||||
span.task-name {
|
||||
display: inline-block;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #fffcaf;
|
||||
.task-actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.task-name {
|
||||
width: 373px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.task-actions {
|
||||
width: 220px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0 0.25em 0 0.25em;
|
||||
line-height: 30px;
|
||||
|
||||
.box-sizing(border-box);
|
||||
position: relative;
|
||||
left: -4px;
|
||||
height: 32px;
|
||||
|
||||
border: 1px solid #d2cd70;
|
||||
.border-radius(4px);
|
||||
-webkit-box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
-moz-box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 104px;
|
||||
cursor: pointer;
|
||||
|
||||
border: 1px solid;
|
||||
.border-radius(4px);
|
||||
.box-shadow(0, 2px, -1px, 0, rgba(0, 0, 0, 0.1));
|
||||
|
||||
.font-verdana();
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||
filter: dropshadow(color=rgba(0, 0, 0, 0.5), offx=1, offy=1);
|
||||
|
||||
&:hover {
|
||||
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
|
||||
inset 0 0 4px rgba(255, 255, 255, 0.6);
|
||||
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
|
||||
inset 0 0 4px rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
|
||||
inset 0 0 4px rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
&:focus {
|
||||
.box-shadow(0, 0, 3px, 1px, #33a0e8);
|
||||
}
|
||||
&:active {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
vertical-align: -4px;
|
||||
}
|
||||
|
||||
&.task-done-button, &#new-task-button, &.task-active-button {
|
||||
border-color: #448c42;
|
||||
.linear-gradient(#8ed66b, #58ba6d);
|
||||
|
||||
&:active {
|
||||
.linear-gradient(#58ba6d, #8ed66b);
|
||||
}
|
||||
}
|
||||
|
||||
&.task-remove-button, &#error-refresh-button, &#signout-button {
|
||||
border-color: #a73030;
|
||||
.linear-gradient(#f67f73, #bb5757);
|
||||
&:active {
|
||||
.linear-gradient(#bb5757, #f67f73);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,10 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="100px" height="87.5px" viewBox="0 0 100 87.5" style="enable-background:new 0 0 100 87.5;" xml:space="preserve">
|
||||
<path style="fill:#010101;" d="M12.5,6.25c0,3.455-2.795,6.25-6.25,6.25C2.795,12.5,0,9.705,0,6.25C0,2.795,2.795,0,6.25,0 C9.705,0,12.5,2.795,12.5,6.25z"/>
|
||||
<rect x="25" style="fill:#010101;" width="75" height="12.5"/>
|
||||
<path style="fill:#010101;" d="M12.5,56.25c0,3.455-2.795,6.25-6.25,6.25C2.795,62.5,0,59.705,0,56.25S2.795,50,6.25,50 C9.705,50,12.5,52.795,12.5,56.25z"/>
|
||||
<rect x="25" y="50" style="fill:#010101;" width="75" height="12.5"/>
|
||||
<path style="fill:#010101;" d="M12.5,31.25c0,3.455-2.795,6.25-6.25,6.25C2.795,37.5,0,34.705,0,31.25S2.795,25,6.25,25 C9.705,25,12.5,27.795,12.5,31.25z"/>
|
||||
<path style="fill:#010101;" d="M12.5,81.25c0,3.455-2.795,6.25-6.25,6.25C2.795,87.5,0,84.705,0,81.25S2.795,75,6.25,75 C9.705,75,12.5,77.795,12.5,81.25z"/>
|
||||
<rect x="25" y="25" style="fill:#010101;" width="75" height="12.5"/>
|
||||
<rect x="25" y="75" style="fill:#010101;" width="75" height="12.5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 124 B |
|
Before Width: | Height: | Size: 326 B |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,68 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- vim: set tabstop=2 shiftwidth=2 softtabstop=2 expandtab : -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Checkbox - dropbox.js Sample Application</title>
|
||||
<link rel="icon" type="image/png" href="images/icon16.png" />
|
||||
<link rel="stylesheet/less" type="text/css" href="./checkbox.less" />
|
||||
<script type="text/javascript" src="lib/coffee-script.js"></script>
|
||||
<script type="text/javascript" src="lib/dropbox.js"></script>
|
||||
<script type="text/javascript" src="lib/jquery.js"></script>
|
||||
<script type="text/javascript" src="lib/less.js"></script>
|
||||
<script type="text/coffeescript" src="./checkbox.coffee"></script>
|
||||
</head>
|
||||
<body>
|
||||
<aside id="error-notice" class="hidden">
|
||||
<form action="#" method="GET">
|
||||
Something went wrong :(
|
||||
<button type="submit" id="error-refresh-button">
|
||||
<img src="images/not_done.png" alt="" /> reload the app
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
<article id="app-ui" class="hidden">
|
||||
<h1>
|
||||
checkbox
|
||||
<small>powered by
|
||||
<a href="https://www.dropbox.com/developers">dropbox</a>
|
||||
</small>
|
||||
</h1>
|
||||
<div id="notebook-page">
|
||||
<aside id="user-info">
|
||||
<button type="button" id="signout-button">
|
||||
<img src="images/remove.png" alt="" /> Sign out
|
||||
</button>
|
||||
<span id="user-name" />
|
||||
</aside>
|
||||
<h2 id="active-task-heading">Active</h2>
|
||||
<ol class="task-list" id="active-task-list"></ol>
|
||||
<form action="" method="GET" id="new-task-form">
|
||||
<input type="text" id="new-task-name" class="task-name"
|
||||
required="required" placeholder="e.g., buy milk" />
|
||||
<button type="submit" id="new-task-button">
|
||||
<img src="images/add.png" alt="" /> Add
|
||||
</button>
|
||||
</form>
|
||||
<div class="empty-task"> </div>
|
||||
<h2 id="done-task-heading">Done</h2>
|
||||
<ol class="task-list" id="done-task-list"></ol>
|
||||
</div>
|
||||
</article>
|
||||
<script type="text/html" id="task-template">
|
||||
<li class="task">
|
||||
<span class="task-name" />
|
||||
<span class="task-actions">
|
||||
<button type="button" class="task-done-button">
|
||||
<img src="images/done.png" alt="" /> Done
|
||||
</button>
|
||||
<button type="button" class="task-active-button">
|
||||
<img src="images/not_done.png" alt="" /> Undo
|
||||
</button>
|
||||
<button type="button" class="task-remove-button">
|
||||
<img src="images/remove.png" alt="" /> Delete
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Main entry point to the Dropbox API.
|
||||
class Dropbox
|
||||
constructor: (options) ->
|
||||
@client = new DropboxClient options
|
||||
|
||||
# NOTE: this is not yet implemented.
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# Information about a failed call to the Dropbox API.
|
||||
class Dropbox.ApiError
|
||||
# @property {Number} the HTTP error code (e.g., 403)
|
||||
status: undefined
|
||||
|
||||
# @property {String} the HTTP method of the failed request (e.g., 'GET')
|
||||
method: undefined
|
||||
|
||||
# @property {String} the URL of the failed request
|
||||
url: undefined
|
||||
|
||||
# @property {?String} the body of the HTTP error response; can be null if
|
||||
# the error was caused by a network failure or by a security issue
|
||||
responseText: undefined
|
||||
|
||||
# @property {?Object} the result of parsing the JSON in the HTTP error
|
||||
# response; can be null if the API server didn't return JSON, or if the
|
||||
# HTTP response body is unavailable
|
||||
response: undefined
|
||||
|
||||
# Wraps a failed XHR call to the Dropbox API.
|
||||
#
|
||||
# @param {String} method the HTTP verb of the API request (e.g., 'GET')
|
||||
# @param {String} url the URL of the API request
|
||||
# @param {XMLHttpRequest} xhr the XMLHttpRequest instance of the failed
|
||||
# request
|
||||
constructor: (xhr, @method, @url) ->
|
||||
@status = xhr.status
|
||||
if xhr.responseType
|
||||
try
|
||||
text = xhr.response or xhr.responseText
|
||||
catch e
|
||||
try
|
||||
text = xhr.responseText
|
||||
catch e
|
||||
text = null
|
||||
else
|
||||
try
|
||||
text = xhr.responseText
|
||||
catch e
|
||||
text = null
|
||||
|
||||
if text
|
||||
try
|
||||
@responseText = text.toString()
|
||||
@response = JSON.parse text
|
||||
catch e
|
||||
@response = null
|
||||
else
|
||||
@responseText = '(no response)'
|
||||
@response = null
|
||||
|
||||
# Used when the error is printed out by developers.
|
||||
toString: ->
|
||||
"Dropbox API error #{@status} from #{@method} #{@url} :: #{@responseText}"
|
||||
|
||||
# Used by some testing frameworks.
|
||||
inspect: ->
|
||||
@toString()
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# node.js implementation of atob and btoa
|
||||
|
||||
if window?
|
||||
if window.atob and window.btoa
|
||||
atob = (string) -> window.atob string
|
||||
btoa = (base64) -> window.btoa base64
|
||||
else
|
||||
# IE < 10 doesn't implement the standard atob / btoa functions.
|
||||
base64Digits =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
|
||||
btoaNibble = (accumulator, bytes, result) ->
|
||||
limit = 3 - bytes
|
||||
accumulator <<= limit * 8
|
||||
i = 3
|
||||
while i >= limit
|
||||
result.push base64Digits.charAt((accumulator >> (i * 6)) & 0x3F)
|
||||
i -= 1
|
||||
i = bytes
|
||||
while i < 3
|
||||
result.push '='
|
||||
i += 1
|
||||
null
|
||||
atobNibble = (accumulator, digits, result) ->
|
||||
limit = 4 - digits
|
||||
accumulator <<= limit * 6
|
||||
i = 2
|
||||
while i >= limit
|
||||
result.push String.fromCharCode((accumulator >> (8 * i)) & 0xFF)
|
||||
i -= 1
|
||||
null
|
||||
|
||||
btoa = (string) ->
|
||||
result = []
|
||||
accumulator = 0
|
||||
bytes = 0
|
||||
for i in [0...string.length]
|
||||
accumulator = (accumulator << 8) | string.charCodeAt(i)
|
||||
bytes += 1
|
||||
if bytes is 3
|
||||
btoaNibble accumulator, bytes, result
|
||||
accumulator = bytes = 0
|
||||
|
||||
if bytes > 0
|
||||
btoaNibble accumulator, bytes, result
|
||||
result.join ''
|
||||
|
||||
atob = (base64) ->
|
||||
result = []
|
||||
accumulator = 0
|
||||
digits = 0
|
||||
for i in [0...base64.length]
|
||||
digit = base64.charAt i
|
||||
break if digit is '='
|
||||
accumulator = (accumulator << 6) | base64Digits.indexOf(digit)
|
||||
digits += 1
|
||||
if digits is 4
|
||||
atobNibble accumulator, digits, result
|
||||
accumulator = digits = 0
|
||||
|
||||
if digits > 0
|
||||
atobNibble accumulator, digits, result
|
||||
result.join ''
|
||||
|
||||
else
|
||||
# NOTE: the npm packages atob and btoa don't do base64-encoding correctly.
|
||||
atob = (arg) ->
|
||||
buffer = new Buffer arg, 'base64'
|
||||
(String.fromCharCode(buffer[i]) for i in [0...buffer.length]).join ''
|
||||
btoa = (arg) ->
|
||||
buffer = new Buffer(arg.charCodeAt(i) for i in [0...arg.length])
|
||||
buffer.toString 'base64'
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
# Base class for drivers that run in the browser.
|
||||
#
|
||||
# Inheriting from this class makes a driver use HTML5 localStorage to preserve
|
||||
# OAuth tokens across page reloads.
|
||||
class Dropbox.Drivers.BrowserBase
|
||||
# Sets up the OAuth driver.
|
||||
#
|
||||
# Subclasses should pass the options object they receive to the superclass
|
||||
# constructor.
|
||||
#
|
||||
# @param {?Object} options the advanced settings below
|
||||
# @option options {Boolean} rememberUser if true, the user's OAuth tokens are
|
||||
# saved in localStorage; if you use this, you MUST provide a UI item that
|
||||
# calls signOut() on Dropbox.Client, to let the user "log out" of the
|
||||
# application
|
||||
# @option options {String} scope embedded in the localStorage key that holds
|
||||
# the authentication data; useful for having multiple OAuth tokens in a
|
||||
# single application
|
||||
constructor: (options) ->
|
||||
@rememberUser = options?.rememberUser or false
|
||||
@scope = options?.scope or 'default'
|
||||
@storageKey = null
|
||||
|
||||
# The magic happens here.
|
||||
onAuthStateChange: (client, callback) ->
|
||||
@setStorageKey client
|
||||
|
||||
switch client.authState
|
||||
when DropboxClient.RESET
|
||||
@loadCredentials (credentials) =>
|
||||
return callback() unless credentials
|
||||
|
||||
if credentials.authState # Incomplete authentication.
|
||||
client.setCredentials credentials
|
||||
return callback()
|
||||
|
||||
# There is an old access token. Only use it if the app supports
|
||||
# logout.
|
||||
unless @rememberUser
|
||||
@forgetCredentials()
|
||||
return callback()
|
||||
|
||||
# Verify that the old access token still works.
|
||||
client.setCredentials credentials
|
||||
client.getUserInfo (error) =>
|
||||
if error
|
||||
client.reset()
|
||||
@forgetCredentials callback
|
||||
else
|
||||
callback()
|
||||
when DropboxClient.REQUEST
|
||||
@storeCredentials client.credentials(), callback
|
||||
when DropboxClient.DONE
|
||||
if @rememberUser
|
||||
return @storeCredentials(client.credentials(), callback)
|
||||
@forgetCredentials callback
|
||||
when DropboxClient.SIGNED_OFF
|
||||
@forgetCredentials callback
|
||||
when DropboxClient.ERROR
|
||||
@forgetCredentials callback
|
||||
else
|
||||
callback()
|
||||
@
|
||||
|
||||
# Computes the @storageKey used by loadCredentials and forgetCredentials.
|
||||
#
|
||||
# @private
|
||||
# This is called by onAuthStateChange.
|
||||
#
|
||||
# @param {Dropbox.Client} client the client instance that is running the
|
||||
# authorization process
|
||||
# @return {Dropbox.Driver} this, for easy call chaining
|
||||
setStorageKey: (client) ->
|
||||
# NOTE: the storage key is dependent on the app hash so that multiple apps
|
||||
# hosted off the same server don't step on eachother's toes
|
||||
@storageKey = "dropbox-auth:#{@scope}:#{client.appHash()}"
|
||||
@
|
||||
|
||||
# Stores a Dropbox.Client's credentials to localStorage.
|
||||
#
|
||||
# @private
|
||||
# onAuthStateChange calls this method during the authentication flow.
|
||||
#
|
||||
# @param {Object} credentials the result of a Drobpox.Client#credentials call
|
||||
# @param {function()} callback called when the storing operation is complete
|
||||
# @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
|
||||
storeCredentials: (credentials, callback) ->
|
||||
localStorage.setItem @storageKey, JSON.stringify(credentials)
|
||||
callback()
|
||||
@
|
||||
|
||||
# Retrieves a token and secret from localStorage.
|
||||
#
|
||||
# @private
|
||||
# onAuthStateChange calls this method during the authentication flow.
|
||||
#
|
||||
# @param {function(?Object)} callback supplied with the credentials object
|
||||
# stored by a previous call to
|
||||
# Dropbox.Drivers.BrowserBase#storeCredentials; null if no credentials were
|
||||
# stored, or if the previously stored credentials were deleted
|
||||
# @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
|
||||
loadCredentials: (callback) ->
|
||||
jsonString = localStorage.getItem @storageKey
|
||||
unless jsonString
|
||||
callback null
|
||||
return @
|
||||
|
||||
try
|
||||
callback JSON.parse(jsonString)
|
||||
catch e
|
||||
# Parse errors.
|
||||
callback null
|
||||
@
|
||||
|
||||
# Deletes information previously stored by a call to storeCredentials.
|
||||
#
|
||||
# @private
|
||||
# onAuthStateChange calls this method during the authentication flow.
|
||||
#
|
||||
# @param {function()} callback called after the credentials are deleted
|
||||
# @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
|
||||
forgetCredentials: (callback) ->
|
||||
localStorage.removeItem @storageKey
|
||||
callback()
|
||||
@
|
||||
|
||||
# Wrapper for window.location, for testing purposes.
|
||||
#
|
||||
# @return {String} the current page's URL
|
||||
@currentLocation: ->
|
||||
window.location.href
|
||||
|
||||
# OAuth driver that uses a redirect and localStorage to complete the flow.
|
||||
class Dropbox.Drivers.Redirect extends Dropbox.Drivers.BrowserBase
|
||||
# Sets up the redirect-based OAuth driver.
|
||||
#
|
||||
# @param {?Object} options the advanced settings below
|
||||
# @option options {Boolean} useQuery if true, the page will receive OAuth
|
||||
# data as query parameters; by default, the page receives OAuth data in
|
||||
# the fragment part of the URL (the string following the #,
|
||||
# available as document.location.hash), to avoid confusing the server
|
||||
# generating the page
|
||||
# @option options {Boolean} rememberUser if true, the user's OAuth tokens are
|
||||
# saved in localStorage; if you use this, you MUST provide a UI item that
|
||||
# calls signOut() on Dropbox.Client, to let the user "log out" of the
|
||||
# application
|
||||
# @option options {String} scope embedded in the localStorage key that holds
|
||||
# the authentication data; useful for having multiple OAuth tokens in a
|
||||
# single application
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@useQuery = options?.useQuery or false
|
||||
@receiverUrl = @computeUrl options
|
||||
@tokenRe = new RegExp "(#|\\?|&)oauth_token=([^&#]+)(&|#|$)"
|
||||
|
||||
# Forwards the authentication process from REQUEST to AUTHORIZED on redirect.
|
||||
onAuthStateChange: (client, callback) ->
|
||||
superCall = do => => super client, callback
|
||||
@setStorageKey client
|
||||
if client.authState is DropboxClient.RESET
|
||||
@loadCredentials (credentials) =>
|
||||
if credentials and credentials.authState # Incomplete authentication.
|
||||
if credentials.token is @locationToken() and
|
||||
credentials.authState is DropboxClient.REQUEST
|
||||
# locationToken matched, so the redirect happened
|
||||
credentials.authState = DropboxClient.AUTHORIZED
|
||||
return @storeCredentials credentials, superCall
|
||||
else
|
||||
# The authentication process broke down, start over.
|
||||
return @forgetCredentials superCall
|
||||
superCall()
|
||||
else
|
||||
superCall()
|
||||
|
||||
# URL of the current page, since the user will be sent right back.
|
||||
url: ->
|
||||
@receiverUrl
|
||||
|
||||
# Redirects to the authorize page.
|
||||
doAuthorize: (authUrl) ->
|
||||
window.location.assign authUrl
|
||||
|
||||
# Pre-computes the return value of url.
|
||||
computeUrl: ->
|
||||
querySuffix = "_dropboxjs_scope=#{encodeURIComponent @scope}"
|
||||
location = Dropbox.Drivers.BrowserBase.currentLocation()
|
||||
if location.indexOf('#') is -1
|
||||
fragment = null
|
||||
else
|
||||
locationPair = location.split '#', 2
|
||||
location = locationPair[0]
|
||||
fragment = locationPair[1]
|
||||
if @useQuery
|
||||
if location.indexOf('?') is -1
|
||||
location += "?#{querySuffix}" # No query string in the URL.
|
||||
else
|
||||
location += "&#{querySuffix}" # The URL already has a query string.
|
||||
else
|
||||
fragment = "?#{querySuffix}"
|
||||
|
||||
if fragment
|
||||
location + '#' + fragment
|
||||
else
|
||||
location
|
||||
|
||||
# Figures out if the user completed the OAuth flow based on the current URL.
|
||||
#
|
||||
# @return {?String} the OAuth token that the user just authorized, or null if
|
||||
# the user accessed this directly, without having authorized a token
|
||||
locationToken: ->
|
||||
location = Dropbox.Drivers.BrowserBase.currentLocation()
|
||||
|
||||
# Check for the scope.
|
||||
scopePattern = "_dropboxjs_scope=#{encodeURIComponent @scope}&"
|
||||
return null if location.indexOf?(scopePattern) is -1
|
||||
|
||||
# Extract the token.
|
||||
match = @tokenRe.exec location
|
||||
if match then decodeURIComponent(match[2]) else null
|
||||
|
||||
# OAuth driver that uses a popup window and postMessage to complete the flow.
|
||||
class Dropbox.Drivers.Popup extends Dropbox.Drivers.BrowserBase
|
||||
# Sets up a popup-based OAuth driver.
|
||||
#
|
||||
# @param {?Object} options one of the settings below; leave out the argument
|
||||
# to use the current location for redirecting
|
||||
# @option options {Boolean} rememberUser if true, the user's OAuth tokens are
|
||||
# saved in localStorage; if you use this, you MUST provide a UI item that
|
||||
# calls signOut() on Dropbox.Client, to let the user "log out" of the
|
||||
# application
|
||||
# @option options {String} scope embedded in the localStorage key that holds
|
||||
# the authentication data; useful for having multiple OAuth tokens in a
|
||||
# single application
|
||||
# @option options {String} receiverUrl URL to the page that receives the
|
||||
# /authorize redirect and performs the postMessage
|
||||
# @option options {Boolean} noFragment if true, the receiverUrl will be used
|
||||
# as given; by default, a hash "#" is appended to URLs that don't have
|
||||
# one, so the OAuth token is received as a URL fragment and does not hit
|
||||
# the file server
|
||||
# @option options {String} receiverFile the URL to the receiver page will be
|
||||
# computed by replacing the file name (everything after the last /) of
|
||||
# the current location with this parameter's value
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@receiverUrl = @computeUrl options
|
||||
@tokenRe = new RegExp "(#|\\?|&)oauth_token=([^&#]+)(&|#|$)"
|
||||
|
||||
# Removes credentials stuck in the REQUEST stage.
|
||||
onAuthStateChange: (client, callback) ->
|
||||
superCall = do => => super client, callback
|
||||
@setStorageKey client
|
||||
if client.authState is DropboxClient.RESET
|
||||
@loadCredentials (credentials) =>
|
||||
if credentials and credentials.authState # Incomplete authentication.
|
||||
# The authentication process broke down, start over.
|
||||
return @forgetCredentials superCall
|
||||
superCall()
|
||||
else
|
||||
superCall()
|
||||
|
||||
# Shows the authorization URL in a pop-up, waits for it to send a message.
|
||||
doAuthorize: (authUrl, token, tokenSecret, callback) ->
|
||||
@listenForMessage token, callback
|
||||
@openWindow authUrl
|
||||
|
||||
# URL of the redirect receiver page, which posts a message back to this page.
|
||||
url: ->
|
||||
@receiverUrl
|
||||
|
||||
# Pre-computes the return value of url.
|
||||
computeUrl: (options) ->
|
||||
if options
|
||||
if options.receiverUrl
|
||||
if options.noFragment or options.receiverUrl.indexOf('#') isnt -1
|
||||
return options.receiverUrl
|
||||
else
|
||||
return options.receiverUrl + '#'
|
||||
else if options.receiverFile
|
||||
fragments = Dropbox.Drivers.BrowserBase.currentLocation().split '/'
|
||||
fragments[fragments.length - 1] = options.receiverFile
|
||||
if options.noFragment
|
||||
return fragments.join('/')
|
||||
else
|
||||
return fragments.join('/') + '#'
|
||||
Dropbox.Drivers.BrowserBase.currentLocation()
|
||||
|
||||
# Creates a popup window.
|
||||
#
|
||||
# @param {String} url the URL that will be loaded in the popup window
|
||||
# @return {?DOMRef} reference to the opened window, or null if the call
|
||||
# failed
|
||||
openWindow: (url) ->
|
||||
window.open url, '_dropboxOauthSigninWindow', @popupWindowSpec(980, 700)
|
||||
|
||||
# Spec string for window.open to create a nice popup.
|
||||
#
|
||||
# @param {Number} popupWidth the desired width of the popup window
|
||||
# @param {Number} popupHeight the desired height of the popup window
|
||||
# @return {String} spec string for the popup window
|
||||
popupWindowSpec: (popupWidth, popupHeight) ->
|
||||
# Metrics for the current browser window.
|
||||
x0 = window.screenX ? window.screenLeft
|
||||
y0 = window.screenY ? window.screenTop
|
||||
width = window.outerWidth ? document.documentElement.clientWidth
|
||||
height = window.outerHeight ? document.documentElement.clientHeight
|
||||
|
||||
# Computed popup window metrics.
|
||||
popupLeft = Math.round x0 + (width - popupWidth) / 2
|
||||
popupTop = Math.round y0 + (height - popupHeight) / 2.5
|
||||
popupLeft = x0 if popupLeft < x0
|
||||
popupTop = y0 if popupTop < y0
|
||||
|
||||
# The specification string.
|
||||
"width=#{popupWidth},height=#{popupHeight}," +
|
||||
"left=#{popupLeft},top=#{popupTop}" +
|
||||
'dialog=yes,dependent=yes,scrollbars=yes,location=yes'
|
||||
|
||||
# Listens for a postMessage from a previously opened popup window.
|
||||
#
|
||||
# @param {String} token the token string that must be received from the popup
|
||||
# window
|
||||
# @param {function()} called when the received message matches the token
|
||||
listenForMessage: (token, callback) ->
|
||||
listener = (event) =>
|
||||
match = @tokenRe.exec event.data.toString()
|
||||
if match and decodeURIComponent(match[2]) is token
|
||||
window.removeEventListener 'message', listener
|
||||
callback()
|
||||
window.addEventListener 'message', listener, false
|
||||
|
||||
# Communicates with the driver from the OAuth receiver page.
|
||||
@oauthReceiver: ->
|
||||
window.addEventListener 'load', ->
|
||||
opener = window.opener
|
||||
if window.parent isnt window.top
|
||||
opener or= window.parent
|
||||
if opener
|
||||
try
|
||||
opener.postMessage window.location.href, '*'
|
||||
catch e
|
||||
# IE 9 doesn't support opener.postMessage for popup windows.
|
||||
window.close()
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
DropboxChromeOnMessage = null
|
||||
DropboxChromeSendMessage = null
|
||||
|
||||
if chrome?
|
||||
# v2 manifest APIs.
|
||||
if chrome.runtime
|
||||
if chrome.runtime.onMessage
|
||||
DropboxChromeOnMessage = chrome.runtime.onMessage
|
||||
if chrome.runtime.sendMessage
|
||||
DropboxChromeSendMessage = (m) -> chrome.runtime.sendMessage m
|
||||
|
||||
# v1 manifest APIs.
|
||||
if chrome.extension
|
||||
if chrome.extension.onMessage
|
||||
DropboxChromeOnMessage or= chrome.extension.onMessage
|
||||
if chrome.extension.sendMessage
|
||||
DropboxChromeSendMessage or= (m) -> chrome.extension.sendMessage m
|
||||
|
||||
# Apps that use the v2 manifest don't get messenging in Chrome 25.
|
||||
unless DropboxChromeOnMessage
|
||||
do ->
|
||||
pageHack = (page) ->
|
||||
if page.Dropbox
|
||||
Dropbox.Drivers.Chrome::onMessage =
|
||||
page.Dropbox.Drivers.Chrome.onMessage
|
||||
Dropbox.Drivers.Chrome::sendMessage =
|
||||
page.Dropbox.Drivers.Chrome.sendMessage
|
||||
else
|
||||
page.Dropbox = Dropbox
|
||||
Dropbox.Drivers.Chrome::onMessage = new Dropbox.EventSource
|
||||
Dropbox.Drivers.Chrome::sendMessage =
|
||||
(m) -> Dropbox.Drivers.Chrome::onMessage.dispatch m
|
||||
|
||||
if chrome.extension and chrome.extension.getBackgroundPage
|
||||
if page = chrome.extension.getBackgroundPage()
|
||||
return pageHack(page)
|
||||
|
||||
if chrome.runtime and chrome.runtime.getBackgroundPage
|
||||
return chrome.runtime.getBackgroundPage (page) -> pageHack page
|
||||
|
||||
# OAuth driver specialized for Chrome apps and extensions.
|
||||
class Dropbox.Drivers.Chrome
|
||||
# @property {Chrome.Event<>, Dropbox.EventSource<>} fires non-cancelable
|
||||
# events when Dropbox.Drivers.Chrome#sendMessage is called
|
||||
onMessage: DropboxChromeOnMessage
|
||||
|
||||
# Sends a message across the Chrome extension / application.
|
||||
#
|
||||
# When a message is sent, the listeners registered to
|
||||
#
|
||||
# @param {Object} message the message to be sent
|
||||
sendMessage: DropboxChromeSendMessage
|
||||
|
||||
# Expans an URL relative to the Chrome extension / application root.
|
||||
#
|
||||
# @param {String} url a resource URL relative to the extension root
|
||||
# @return {String} the absolute resource URL
|
||||
expandUrl: (url) ->
|
||||
if chrome.runtime and chrome.runtime.getURL
|
||||
return chrome.runtime.getURL(url)
|
||||
if chrome.extension and chrome.extension.getURL
|
||||
return chrome.extension.getURL(url)
|
||||
url
|
||||
|
||||
# @param {?Object} options the settings below
|
||||
# @option {String} receiverPath the path of page that receives the /authorize
|
||||
# redirect and performs the postMessage; the path should be relative to the
|
||||
# extension folder; by default, is 'chrome_oauth_receiver.html'
|
||||
constructor: (options) ->
|
||||
receiverPath = (options and options.receiverPath) or
|
||||
'chrome_oauth_receiver.html'
|
||||
@receiverUrl = @expandUrl receiverPath
|
||||
@tokenRe = new RegExp "(#|\\?|&)oauth_token=([^&#]+)(&|#|$)"
|
||||
scope = (options and options.scope) or 'default'
|
||||
@storageKey = "dropbox_js_#{scope}_credentials"
|
||||
|
||||
# Saves token information when appropriate.
|
||||
onAuthStateChange: (client, callback) ->
|
||||
switch client.authState
|
||||
when Dropbox.Client.RESET
|
||||
@loadCredentials (credentials) =>
|
||||
if credentials
|
||||
if credentials.authState
|
||||
# Stuck authentication process, reset.
|
||||
return @forgetCredentials(callback)
|
||||
client.setCredentials credentials
|
||||
callback()
|
||||
when Dropbox.Client.DONE
|
||||
@storeCredentials client.credentials(), callback
|
||||
when Dropbox.Client.SIGNED_OFF
|
||||
@forgetCredentials callback
|
||||
when Dropbox.Client.ERROR
|
||||
@forgetCredentials callback
|
||||
else
|
||||
callback()
|
||||
|
||||
# Shows the authorization URL in a pop-up, waits for it to send a message.
|
||||
doAuthorize: (authUrl, token, tokenSecret, callback) ->
|
||||
window = handle: null
|
||||
@listenForMessage token, window, callback
|
||||
@openWindow authUrl, (handle) -> window.handle = handle
|
||||
|
||||
# Creates a popup window.
|
||||
#
|
||||
# @param {String} url the URL that will be loaded in the popup window
|
||||
# @param {function(Object)} callback called with a handle that can be passed
|
||||
# to Dropbox.Driver.Chrome#closeWindow
|
||||
# @return {Dropbox.Driver.Chrome} this
|
||||
openWindow: (url, callback) ->
|
||||
if chrome.tabs and chrome.tabs.create
|
||||
chrome.tabs.create url: url, active: true, pinned: false, (tab) ->
|
||||
callback tab
|
||||
return @
|
||||
if chrome.app and chrome.app.window and chrome.app.window.create
|
||||
chrome.app.window.create url, frame: 'none', id: 'dropbox-auth',
|
||||
(window) -> callback window
|
||||
return @
|
||||
@
|
||||
|
||||
# Closes a window that was previously opened with openWindow.
|
||||
#
|
||||
# @param {Object} handle the object passed to an openWindow callback
|
||||
closeWindow: (handle) ->
|
||||
if chrome.tabs and chrome.tabs.remove and handle.id
|
||||
chrome.tabs.remove handle.id
|
||||
return @
|
||||
if chrome.app and chrome.app.window and handle.close
|
||||
handle.close()
|
||||
return @
|
||||
@
|
||||
|
||||
# URL of the redirect receiver page that messages the app / extension.
|
||||
url: ->
|
||||
@receiverUrl
|
||||
|
||||
# Listens for a postMessage from a previously opened tab.
|
||||
#
|
||||
# @param {String} token the token string that must be received from the tab
|
||||
# @param {Object} window a JavaScript object whose "handle" property is a
|
||||
# window handle passed to the callback of a
|
||||
# Dropbox.Driver.Chrome#openWindow call
|
||||
# @param {function()} called when the received message matches the token
|
||||
listenForMessage: (token, window, callback) ->
|
||||
listener = (message, sender) =>
|
||||
# Reject messages not coming from the OAuth receiver window.
|
||||
if sender and sender.tab
|
||||
unless sender.tab.url.substring(0, @receiverUrl.length) is @receiverUrl
|
||||
return
|
||||
|
||||
match = @tokenRe.exec message.dropbox_oauth_receiver_href or ''
|
||||
if match and decodeURIComponent(match[2]) is token
|
||||
@closeWindow window.handle if window.handle
|
||||
@onMessage.removeListener listener
|
||||
callback()
|
||||
@onMessage.addListener listener
|
||||
|
||||
# Stores a Dropbox.Client's credentials to local storage.
|
||||
#
|
||||
# @private
|
||||
# onAuthStateChange calls this method during the authentication flow.
|
||||
#
|
||||
# @param {Object} credentials the result of a Drobpox.Client#credentials call
|
||||
# @param {function()} callback called when the storing operation is complete
|
||||
# @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
|
||||
storeCredentials: (credentials, callback) ->
|
||||
items= {}
|
||||
items[@storageKey] = credentials
|
||||
chrome.storage.local.set items, callback
|
||||
@
|
||||
|
||||
# Retrieves a token and secret from localStorage.
|
||||
#
|
||||
# @private
|
||||
# onAuthStateChange calls this method during the authentication flow.
|
||||
#
|
||||
# @param {function(?Object)} callback supplied with the credentials object
|
||||
# stored by a previous call to
|
||||
# Dropbox.Drivers.BrowserBase#storeCredentials; null if no credentials were
|
||||
# stored, or if the previously stored credentials were deleted
|
||||
# @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
|
||||
loadCredentials: (callback) ->
|
||||
chrome.storage.local.get @storageKey, (items) =>
|
||||
callback items[@storageKey] or null
|
||||
@
|
||||
|
||||
# Deletes information previously stored by a call to storeCredentials.
|
||||
#
|
||||
# @private
|
||||
# onAuthStateChange calls this method during the authentication flow.
|
||||
#
|
||||
# @param {function()} callback called after the credentials are deleted
|
||||
# @return {Dropbox.Drivers.BrowserBase} this, for easy call chaining
|
||||
forgetCredentials: (callback) ->
|
||||
chrome.storage.local.remove @storageKey, callback
|
||||
@
|
||||
|
||||
# Communicates with the driver from the OAuth receiver page.
|
||||
@oauthReceiver: ->
|
||||
window.addEventListener 'load', ->
|
||||
driver = new Dropbox.Drivers.Chrome()
|
||||
driver.sendMessage dropbox_oauth_receiver_href: window.location.href
|
||||
window.close() if window.close
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# OAuth driver that redirects the browser to a node app to complete the flow.
|
||||
#
|
||||
# This is useful for testing node.js libraries and applications.
|
||||
class Dropbox.Drivers.NodeServer
|
||||
# Starts up the node app that intercepts the browser redirect.
|
||||
#
|
||||
# @param {?Object} options one or more of the options below
|
||||
# @option options {Number} port the number of the TCP port that will receive
|
||||
# HTTP requests
|
||||
# @param {String} faviconFile the path to a file that will be served at
|
||||
# /favicon.ico
|
||||
constructor: (options) ->
|
||||
@port = options?.port or 8912
|
||||
@faviconFile = options?.favicon or null
|
||||
# Calling require in the constructor because this doesn't work in browsers.
|
||||
@fs = require 'fs'
|
||||
@http = require 'http'
|
||||
@open = require 'open'
|
||||
|
||||
@callbacks = {}
|
||||
@urlRe = new RegExp "^/oauth_callback\\?"
|
||||
@tokenRe = new RegExp "(\\?|&)oauth_token=([^&]+)(&|$)"
|
||||
@createApp()
|
||||
|
||||
# URL to the node.js OAuth callback handler.
|
||||
url: ->
|
||||
"http://localhost:#{@port}/oauth_callback"
|
||||
|
||||
# Opens the token
|
||||
doAuthorize: (authUrl, token, tokenSecret, callback) ->
|
||||
@callbacks[token] = callback
|
||||
@openBrowser authUrl
|
||||
|
||||
# Opens the given URL in a browser.
|
||||
openBrowser: (url) ->
|
||||
unless url.match /^https?:\/\//
|
||||
throw new Error("Not a http/https URL: #{url}")
|
||||
@open url
|
||||
|
||||
# Creates and starts up an HTTP server that will intercept the redirect.
|
||||
createApp: ->
|
||||
@app = @http.createServer (request, response) =>
|
||||
@doRequest request, response
|
||||
@app.listen @port
|
||||
|
||||
# Shuts down the HTTP server.
|
||||
#
|
||||
# The driver will become unusable after this call.
|
||||
closeServer: ->
|
||||
@app.close()
|
||||
|
||||
# Reads out an /authorize callback.
|
||||
doRequest: (request, response) ->
|
||||
if @urlRe.exec request.url
|
||||
match = @tokenRe.exec request.url
|
||||
if match
|
||||
token = decodeURIComponent match[2]
|
||||
if @callbacks[token]
|
||||
@callbacks[token]()
|
||||
delete @callbacks[token]
|
||||
data = ''
|
||||
request.on 'data', (dataFragment) -> data += dataFragment
|
||||
request.on 'end', =>
|
||||
if @faviconFile and (request.url is '/favicon.ico')
|
||||
@sendFavicon response
|
||||
else
|
||||
@closeBrowser response
|
||||
|
||||
# Renders a response that will close the browser window used for OAuth.
|
||||
closeBrowser: (response) ->
|
||||
closeHtml = """
|
||||
<!doctype html>
|
||||
<script type="text/javascript">window.close();</script>
|
||||
<p>Please close this window.</p>
|
||||
"""
|
||||
response.writeHead(200,
|
||||
{'Content-Length': closeHtml.length, 'Content-Type': 'text/html' })
|
||||
response.write closeHtml
|
||||
response.end
|
||||
|
||||
# Renders the favicon file.
|
||||
sendFavicon: (response) ->
|
||||
@fs.readFile @faviconFile, (error, data) ->
|
||||
response.writeHead(200,
|
||||
{ 'Content-Length': data.length, 'Content-Type': 'image/x-icon' })
|
||||
response.write data
|
||||
response.end
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# Documentation for the interface to a Dropbox OAuth driver.
|
||||
class Dropbox.AuthDriver
|
||||
# The callback URL that should be supplied to the OAuth /authorize call.
|
||||
#
|
||||
# The driver must be able to intercept redirects to the returned URL, in
|
||||
# order to know when a user has completed the authorization flow.
|
||||
#
|
||||
# @return {String} an absolute URL
|
||||
url: ->
|
||||
'https://some.url'
|
||||
|
||||
# Redirects users to /authorize and waits for them to complete the flow.
|
||||
#
|
||||
# This method is called when the OAuth process reaches the REQUEST state,
|
||||
# meaning the client has a request token that must be authorized by the user.
|
||||
#
|
||||
# @param {String} authUrl the URL that users should be sent to in order to
|
||||
# authorize the application's token; this points to a Web page on
|
||||
# Dropbox' servers
|
||||
# @param {String} token the OAuth token that the user is authorizing; this
|
||||
# will be provided by the Dropbox servers as a query parameter when the
|
||||
# user is redirected to the URL returned by the driver's url() method
|
||||
# @param {String} tokenSecret the secret associated with the given OAuth
|
||||
# token; the driver may store this together with the token
|
||||
# @param {function()} callback called when users have completed the
|
||||
# authorization flow; the driver should call this when Dropbox redirects
|
||||
# users to the URL returned by the url() method, and the 'token' query
|
||||
# parameter matches the value of the token parameter
|
||||
doAuthorize: (authUrl, token, tokenSecret, callback) ->
|
||||
callback 'oauth-token'
|
||||
|
||||
# Called when there is some progress in the OAuth process.
|
||||
#
|
||||
# The OAuth process goes through the following states:
|
||||
#
|
||||
# * Dropbox.Client.RESET - the client has no OAuth token, and is about to
|
||||
# ask for a request token
|
||||
# * Dropbox.Client.REQUEST - the client has a request OAuth token, and the
|
||||
# user must go to an URL on the Dropbox servers to authorize the token
|
||||
# * Dropbox.Client.AUTHORIZED - the client has a request OAuth token that
|
||||
# was authorized by the user, and is about to exchange it for an access
|
||||
# token
|
||||
# * Dropbox.Client.DONE - the client has an access OAuth token that can be
|
||||
# used for all API calls; the OAuth process is complete, and the callback
|
||||
# passed to authorize is about to be called
|
||||
# * Dropbox.Client.SIGNED_OFF - the client's Dropbox.Client#signOut() was
|
||||
# called, and the client's OAuth token was invalidated
|
||||
# * Dropbox.Client.ERROR - the client encounered an error during the OAuth
|
||||
# process; the callback passed to authorize is about to be called with the
|
||||
# error information
|
||||
#
|
||||
# @param {Dropbox.Client} client the client performing the OAuth process
|
||||
# @param {function()} callback called when onAuthStateChange acknowledges the
|
||||
# state change
|
||||
onAuthStateChange: (client, callback) ->
|
||||
callback()
|
||||
|
||||
# Namespace for authentication drivers.
|
||||
Dropbox.Drivers = {}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
# Event dispatch following a publisher-subscriber (PubSub) model.
|
||||
class Dropbox.EventSource
|
||||
# Sets up an event source (publisher).
|
||||
#
|
||||
# @param {?Object} options one or more of the options below
|
||||
# @option options {Boolean} cancelable if true,
|
||||
constructor: (options) ->
|
||||
@_cancelable = options and options.cancelable
|
||||
@_listeners = []
|
||||
|
||||
# Registers a listener (subscriber) to events coming from this source.
|
||||
#
|
||||
# This is a simplified version of the addEventListener DOM API. Listeners
|
||||
# must be functions, and they can be removed by calling removeListener.
|
||||
#
|
||||
# This method is idempotent, so a function will not be added to the list of
|
||||
# listeners if was previously added.
|
||||
#
|
||||
# @param {function(Object)} listener called every time an event is fired; if
|
||||
# the event is cancelable, the function can return false to cancel the
|
||||
# event, or any other value to allow it to propagate; the return value is
|
||||
# ignored for non-cancelable events
|
||||
# @return {Dropbox.EventSource} this, for easy call chaining
|
||||
addListener: (listener) ->
|
||||
unless typeof listener is 'function'
|
||||
throw new TypeError 'Invalid listener type; expected function'
|
||||
unless listener in @_listeners
|
||||
@_listeners.push listener
|
||||
@
|
||||
|
||||
# Un-registers a listener (subscriber) previously added by addListener.
|
||||
#
|
||||
# This is a simplified version of the removeEventListener DOM API. The
|
||||
# listener must be exactly the same object supplied to addListener.
|
||||
#
|
||||
# This method is idempotent, so it will fail silently if the given listener
|
||||
# is not registered as a subscriber.
|
||||
#
|
||||
# @param {function(Object)} listener function that was previously passed in
|
||||
# an addListener call
|
||||
# @return {Dropbox.EventSource} this, for easy call chaining
|
||||
removeListener: (listener) ->
|
||||
if @_listeners.indexOf
|
||||
# IE9+
|
||||
index = @_listeners.indexOf listener
|
||||
@_listeners.splice index, 1 if index isnt -1
|
||||
else
|
||||
# IE8 doesn't implement Array#indexOf in ES5.
|
||||
for subscriber, i in @_listeners
|
||||
if subscriber is listener
|
||||
@_listeners.splice i, 1
|
||||
break
|
||||
@
|
||||
|
||||
|
||||
# Informs the listeners (subscribers) that an event occurred.
|
||||
#
|
||||
# Event sources configured for non-cancelable events call all listeners in an
|
||||
# unspecified order. Sources configured for cancelable events stop calling
|
||||
# listeners as soon as one listener returns false value.
|
||||
#
|
||||
# @param {Object} event passed to all the registered listeners
|
||||
# @return {Boolean} sources of cancelable events return false if the event
|
||||
# was canceled and true otherwise; sources of non-cancelable events always
|
||||
# return true
|
||||
dispatch: (event) ->
|
||||
for listener in @_listeners
|
||||
returnValue = listener event
|
||||
if @_cancelable and returnValue is false
|
||||
return false
|
||||
true
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
# HMAC-SHA1 implementation heavily inspired from
|
||||
# http://pajhome.org.uk/crypt/md5/sha1.js
|
||||
|
||||
# Base64-encoded HMAC-SHA1.
|
||||
#
|
||||
# @param {String} string the ASCII string to be signed
|
||||
# @param {String} key the HMAC key
|
||||
# @return {String} a base64-encoded HMAC of the given string and key
|
||||
base64HmacSha1 = (string, key) ->
|
||||
arrayToBase64 hmacSha1(stringToArray(string), stringToArray(key),
|
||||
string.length, key.length)
|
||||
|
||||
# Base64-encoded SHA1.
|
||||
#
|
||||
# @param {String} string the ASCII string to be hashed
|
||||
# @return {String} a base64-encoded SHA1 hash of the given string
|
||||
base64Sha1 = (string) ->
|
||||
arrayToBase64 sha1(stringToArray(string), string.length)
|
||||
|
||||
# SHA1 and HMAC-SHA1 versions that use the node.js builtin crypto.
|
||||
unless window?
|
||||
crypto = require 'crypto'
|
||||
base64HmacSha1 = (string, key) ->
|
||||
hmac = crypto.createHmac 'sha1', key
|
||||
hmac.update string
|
||||
hmac.digest 'base64'
|
||||
base64Sha1 = (string) ->
|
||||
hash = crypto.createHash 'sha1'
|
||||
hash.update string
|
||||
hash.digest 'base64'
|
||||
|
||||
# HMAC-SHA1 implementation.
|
||||
#
|
||||
# @param {Array} string the HMAC input, as an array of 32-bit numbers
|
||||
# @param {Array} key the HMAC input, as an array of 32-bit numbers
|
||||
# @param {Number} length the length of the HMAC input, in bytes
|
||||
# @return {Array} the HMAC output, as an array of 32-bit numbers
|
||||
hmacSha1 = (string, key, length, keyLength) ->
|
||||
if key.length > 16
|
||||
key = sha1 key, keyLength
|
||||
|
||||
ipad = (key[i] ^ 0x36363636 for i in [0...16])
|
||||
opad = (key[i] ^ 0x5C5C5C5C for i in [0...16])
|
||||
|
||||
hash1 = sha1 ipad.concat(string), 64 + length
|
||||
sha1 opad.concat(hash1), 64 + 20
|
||||
|
||||
# SHA1 implementation.
|
||||
#
|
||||
# @param {Array} string the SHA1 input, as an array of 32-bit numbers; the
|
||||
# computation trashes the array
|
||||
# @param {Number} length the number of bytes in the SHA1 input; used in the
|
||||
# SHA1 padding algorithm
|
||||
# @return {Array<Number>} the SHA1 output, as an array of 32-bit numbers
|
||||
sha1 = (string, length) ->
|
||||
string[length >> 2] |= 1 << (31 - ((length & 0x03) << 3))
|
||||
string[(((length + 8) >> 6) << 4) + 15] = length << 3
|
||||
|
||||
state = Array 80
|
||||
a = 1732584193 # 0x67452301
|
||||
b = -271733879 # 0xefcdab89
|
||||
c = -1732584194 # 0x98badcfe
|
||||
d = 271733878 # 0x10325476
|
||||
e = -1009589776 # 0xc3d2e1f0
|
||||
|
||||
i = 0
|
||||
limit = string.length
|
||||
# Uncomment the line below to debug packing.
|
||||
# console.log string.map(xxx)
|
||||
while i < limit
|
||||
a0 = a
|
||||
b0 = b
|
||||
c0 = c
|
||||
d0 = d
|
||||
e0 = e
|
||||
|
||||
for j in [0...80]
|
||||
if j < 16
|
||||
state[j] = string[i + j]
|
||||
else
|
||||
state[j] = rotateLeft32 state[j - 3] ^ state[j - 8] ^ state[j - 14] ^
|
||||
state[j - 16], 1
|
||||
if j < 20
|
||||
ft = (b & c) | ((~b) & d)
|
||||
kt = 1518500249 # 0x5a827999
|
||||
else if j < 40
|
||||
ft = b ^ c ^ d
|
||||
kt = 1859775393 # 0x6ed9eba1
|
||||
else if j < 60
|
||||
ft = (b & c) | (b & d) | (c & d)
|
||||
kt = -1894007588 # 0x8f1bbcdc
|
||||
else
|
||||
ft = b ^ c ^ d
|
||||
kt = -899497514 # 0xca62c1d6
|
||||
t = add32 add32(rotateLeft32(a, 5), ft), add32(add32(e, state[j]), kt)
|
||||
e = d
|
||||
d = c
|
||||
c = rotateLeft32 b, 30
|
||||
b = a
|
||||
a = t
|
||||
# Uncomment the line below to debug block computation.
|
||||
# console.log [xxx(a), xxx(b), xxx(c), xxx(d), xxx(e)]
|
||||
a = add32 a, a0
|
||||
b = add32 b, b0
|
||||
c = add32 c, c0
|
||||
d = add32 d, d0
|
||||
e = add32 e, e0
|
||||
i += 16
|
||||
# Uncomment the line below to see the input to the base64 encoder.
|
||||
# console.log [xxx(a), xxx(b), xxx(c), xxx(d), xxx(e)]
|
||||
[a, b, c, d, e]
|
||||
|
||||
###
|
||||
# Uncomment the definition below for debugging.
|
||||
#
|
||||
# Returns the hexadecimal representation of a 32-bit number.
|
||||
xxx = (n) ->
|
||||
if n < 0
|
||||
n = (1 << 30) * 4 + n
|
||||
n.toString 16
|
||||
###
|
||||
|
||||
# Rotates a 32-bit word.
|
||||
#
|
||||
# @param {Number} value the 32-bit number to be rotated
|
||||
# @param {Number} count the number of bits (0..31) to rotate by
|
||||
# @return {Number} the rotated value
|
||||
rotateLeft32 = (value, count) ->
|
||||
(value << count) | (value >>> (32 - count))
|
||||
|
||||
# 32-bit unsigned addition.
|
||||
#
|
||||
# @param {Number} a, b the 32-bit numbers to be added modulo 2^32
|
||||
# @return {Number} the 32-bit representation of a + b
|
||||
add32 = (a, b) ->
|
||||
low = (a & 0xFFFF) + (b & 0xFFFF)
|
||||
high = (a >> 16) + (b >> 16) + (low >> 16)
|
||||
(high << 16) | (low & 0xFFFF)
|
||||
|
||||
# Converts a 32-bit number array into a base64-encoded string.
|
||||
#
|
||||
# @param {Array} an array of big-endian 32-bit numbers
|
||||
# @return {String} base64 encoding of the given array of numbers
|
||||
arrayToBase64 = (array) ->
|
||||
string = ""
|
||||
i = 0
|
||||
limit = array.length * 4
|
||||
while i < limit
|
||||
i2 = i
|
||||
trit = ((array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF) << 16
|
||||
i2 += 1
|
||||
trit |= ((array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF) << 8
|
||||
i2 += 1
|
||||
trit |= (array[i2 >> 2] >> ((3 - (i2 & 3)) << 3)) & 0xFF
|
||||
|
||||
string += _base64Digits[(trit >> 18) & 0x3F]
|
||||
string += _base64Digits[(trit >> 12) & 0x3F]
|
||||
i += 1
|
||||
if i >= limit
|
||||
string += '='
|
||||
else
|
||||
string += _base64Digits[(trit >> 6) & 0x3F]
|
||||
i += 1
|
||||
if i >= limit
|
||||
string += '='
|
||||
else
|
||||
string += _base64Digits[trit & 0x3F]
|
||||
i += 1
|
||||
string
|
||||
|
||||
_base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
# Converts an ASCII string into array of 32-bit numbers.
|
||||
stringToArray = (string) ->
|
||||
array = []
|
||||
mask = 0xFF
|
||||
for i in [0...string.length]
|
||||
array[i >> 2] |= (string.charCodeAt(i) & mask) << ((3 - (i & 3)) << 3)
|
||||
array
|
||||
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# Stripped-down OAuth implementation that works with the Dropbox API server.
|
||||
class Dropbox.Oauth
|
||||
# Creates an Oauth instance that manages an application's keys and token.
|
||||
#
|
||||
# @param {Object} options the following properties
|
||||
# @option options {String} key the Dropbox application's key (consumer key,
|
||||
# in OAuth vocabulary); browser-side applications should use
|
||||
# Dropbox.encodeKey to obtain an encoded key string, and pass it as the
|
||||
# key option
|
||||
# @option options {String} secret the Dropbox application's secret (consumer
|
||||
# secret, in OAuth vocabulary); browser-side applications should not use
|
||||
# the secret option; instead, they should pass the result of
|
||||
# Dropbox.encodeKey as the key option
|
||||
constructor: (options) ->
|
||||
@key = @k = null
|
||||
@secret = @s = null
|
||||
@token = null
|
||||
@tokenSecret = null
|
||||
@_appHash = null
|
||||
@reset options
|
||||
|
||||
# Creates an Oauth instance that manages an application's keys and token.
|
||||
#
|
||||
# @see Dropbox.Oauth#constructor for options
|
||||
reset: (options) ->
|
||||
if options.secret
|
||||
@k = @key = options.key
|
||||
@s = @secret = options.secret
|
||||
@_appHash = null
|
||||
else if options.key
|
||||
@key = options.key
|
||||
@secret = null
|
||||
secret = atob dropboxEncodeKey(@key).split('|', 2)[1]
|
||||
[k, s] = secret.split '?', 2
|
||||
@k = decodeURIComponent k
|
||||
@s = decodeURIComponent s
|
||||
@_appHash = null
|
||||
else
|
||||
unless @k
|
||||
throw new Error('No API key supplied')
|
||||
|
||||
if options.token
|
||||
@setToken options.token, options.tokenSecret
|
||||
else
|
||||
@setToken null, ''
|
||||
|
||||
# Sets the OAuth token to be used for future requests.
|
||||
setToken: (token, tokenSecret) ->
|
||||
if token and (not tokenSecret)
|
||||
throw new Error('No secret supplied with the user token')
|
||||
|
||||
@token = token
|
||||
@tokenSecret = tokenSecret || ''
|
||||
|
||||
# This is part of signing, but it's set here so it can be cached.
|
||||
@hmacKey = Dropbox.Xhr.urlEncodeValue(@s) + '&' +
|
||||
Dropbox.Xhr.urlEncodeValue(tokenSecret)
|
||||
null
|
||||
|
||||
# Computes the value of the Authorization HTTP header.
|
||||
#
|
||||
# This method mutates the params object, and removes all the OAuth-related
|
||||
# parameters from it.
|
||||
#
|
||||
# @param {String} method the HTTP method used to make the request ('GET',
|
||||
# 'POST', etc)
|
||||
# @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
|
||||
# that receives the request
|
||||
# @param {Object} params an associative array (hash) containing the HTTP
|
||||
# request parameters; the parameters should include the oauth_
|
||||
# parameters generated by calling {Dropbox.Oauth#boilerplateParams}
|
||||
# @return {String} the value to be used for the Authorization HTTP header
|
||||
authHeader: (method, url, params) ->
|
||||
@addAuthParams method, url, params
|
||||
|
||||
# Collect all the OAuth parameters.
|
||||
oauth_params = []
|
||||
for param, value of params
|
||||
if param.substring(0, 6) == 'oauth_'
|
||||
oauth_params.push param
|
||||
oauth_params.sort()
|
||||
|
||||
# Remove the parameters from the params hash and add them to the header.
|
||||
header = []
|
||||
for param in oauth_params
|
||||
header.push Dropbox.Xhr.urlEncodeValue(param) + '="' +
|
||||
Dropbox.Xhr.urlEncodeValue(params[param]) + '"'
|
||||
delete params[param]
|
||||
|
||||
# NOTE: the space after the comma is optional in the OAuth spec, so we'll
|
||||
# skip it to save some bandwidth
|
||||
'OAuth ' + header.join(',')
|
||||
|
||||
# Generates OAuth-required HTTP parameters.
|
||||
#
|
||||
# This method mutates the params object, and adds the OAuth-related
|
||||
# parameters to it.
|
||||
#
|
||||
# @param {String} method the HTTP method used to make the request ('GET',
|
||||
# 'POST', etc)
|
||||
# @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
|
||||
# that receives the request
|
||||
# @param {Object} params an associative array (hash) containing the HTTP
|
||||
# request parameters; the parameters should include the oauth_
|
||||
# parameters generated by calling {Dropbox.Oauth#boilerplateParams}
|
||||
# @return {String} the value to be used for the Authorization HTTP header
|
||||
addAuthParams: (method, url, params) ->
|
||||
# Augment params with OAuth parameters.
|
||||
@boilerplateParams params
|
||||
params.oauth_signature = @signature method, url, params
|
||||
params
|
||||
|
||||
# Adds boilerplate OAuth parameters to a request's parameter list.
|
||||
#
|
||||
# This should be called right before signing a request, to maximize the
|
||||
# chances that the OAuth timestamp will be fresh.
|
||||
#
|
||||
# @param {Object} params an associative array (hash) containing the
|
||||
# parameters for an OAuth request; the boilerplate parameters will be
|
||||
# added to this hash
|
||||
# @return {Object} params
|
||||
boilerplateParams: (params) ->
|
||||
params.oauth_consumer_key = @k
|
||||
params.oauth_nonce = @nonce()
|
||||
params.oauth_signature_method = 'HMAC-SHA1'
|
||||
params.oauth_token = @token if @token
|
||||
params.oauth_timestamp = Math.floor(Date.now() / 1000)
|
||||
params.oauth_version = '1.0'
|
||||
params
|
||||
|
||||
# Generates a nonce for an OAuth request.
|
||||
#
|
||||
# @return {String} the nonce to be used as the oauth_nonce parameter
|
||||
nonce: ->
|
||||
Date.now().toString(36) + Math.random().toString(36)
|
||||
|
||||
# Computes the signature for an OAuth request.
|
||||
#
|
||||
# @param {String} method the HTTP method used to make the request ('GET',
|
||||
# 'POST', etc)
|
||||
# @param {String} url the HTTP URL (e.g. "http://www.example.com/photos")
|
||||
# that receives the request
|
||||
# @param {Object} params an associative array (hash) containing the HTTP
|
||||
# request parameters; the parameters should include the oauth_
|
||||
# parameters generated by calling {Dropbox.Oauth#boilerplateParams}
|
||||
# @return {String} the signature, ready to be used as the oauth_signature
|
||||
# OAuth parameter
|
||||
signature: (method, url, params) ->
|
||||
string = method.toUpperCase() + '&' + Dropbox.Xhr.urlEncodeValue(url) +
|
||||
'&' + Dropbox.Xhr.urlEncodeValue(Dropbox.Xhr.urlEncode(params))
|
||||
base64HmacSha1 string, @hmacKey
|
||||
|
||||
# @return {String} a string that uniquely identifies the OAuth application
|
||||
appHash: ->
|
||||
return @_appHash if @_appHash
|
||||
@_appHash = base64Sha1(@k).replace(/\=/g, '')
|
||||
|
||||
|
||||
# Polyfill for Internet Explorer 8.
|
||||
unless Date.now?
|
||||
Date.now = () ->
|
||||
(new Date()).getTime()
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Necessary bits to get a browser-side app in production.
|
||||
|
||||
# Packs up a key and secret into a string, to bring script kiddies some pain.
|
||||
#
|
||||
# @param {String} key the application's API key
|
||||
# @param {String} secret the application's API secret
|
||||
# @return {String} encoded key string that can be passed as the key option to
|
||||
# the Dropbox.Client constructor
|
||||
dropboxEncodeKey = (key, secret) ->
|
||||
if secret
|
||||
secret = [encodeURIComponent(key), encodeURIComponent(secret)].join('?')
|
||||
key = for i in [0...(key.length / 2)]
|
||||
((key.charCodeAt(i * 2) & 15) * 16) + (key.charCodeAt(i * 2 + 1) & 15)
|
||||
else
|
||||
[key, secret] = key.split '|', 2
|
||||
key = atob key
|
||||
key = (key.charCodeAt(i) for i in [0...key.length])
|
||||
secret = atob secret
|
||||
|
||||
s = [0...256]
|
||||
y = 0
|
||||
for x in [0...256]
|
||||
y = (y + s[i] + key[x % key.length]) % 256
|
||||
[s[x], s[y]] = [s[y], s[x]]
|
||||
|
||||
x = y = 0
|
||||
result = for z in [0...secret.length]
|
||||
x = (x + 1) % 256
|
||||
y = (y + s[x]) % 256
|
||||
[s[x], s[y]] = [s[y], s[x]]
|
||||
k = s[(s[x] + s[y]) % 256]
|
||||
String.fromCharCode((k ^ secret.charCodeAt(z)) % 256)
|
||||
|
||||
key = (String.fromCharCode(key[i]) for i in [0...key.length])
|
||||
[btoa(key.join('')), btoa(result.join(''))].join '|'
|
||||
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
# Wraps the result of pullChanges, describing the changes in a user's Dropbox.
|
||||
class Dropbox.PulledChanges
|
||||
# Creates a new Dropbox.PulledChanges instance from a /delta API call result.
|
||||
#
|
||||
# @param {?Object} deltaInfo the parsed JSON of a /delta API call result
|
||||
# @return {?Dropbox.PulledChanges} a Dropbox.PulledChanges instance wrapping
|
||||
# the given information; if the parameter does not look like parsed JSON,
|
||||
# it is returned as is
|
||||
@parse: (deltaInfo) ->
|
||||
if deltaInfo and typeof deltaInfo is 'object'
|
||||
new Dropbox.PulledChanges deltaInfo
|
||||
else
|
||||
deltaInfo
|
||||
|
||||
# @property {Boolean} if true, the application should reset its copy of the
|
||||
# user's Dropbox before applying the changes described by this instance
|
||||
blankSlate: undefined
|
||||
|
||||
# @property {String} encodes a cursor in the list of changes to a user's
|
||||
# Dropbox; a pullChanges call returns some changes at the cursor, and then
|
||||
# advance the cursor to account for the returned changes; the new cursor is
|
||||
# returned by pullChanges, and meant to be used by a subsequent pullChanges
|
||||
# call
|
||||
cursorTag: undefined
|
||||
|
||||
# @property {Array<Dropbox.PullChange> an array with one entry for each
|
||||
# change to the user's Dropbox returned by a pullChanges call.
|
||||
changes: undefined
|
||||
|
||||
# @property {Boolean} if true, the pullChanges call returned a subset of the
|
||||
# available changes, and the application should repeat the call
|
||||
# immediately to get more changes
|
||||
shouldPullAgain: undefined
|
||||
|
||||
# @property {Boolean} if true, the API call will not have any more changes
|
||||
# available in the nearby future, so the application should wait for at
|
||||
# least 5 miuntes before issuing another pullChanges request
|
||||
shouldBackOff: undefined
|
||||
|
||||
# Serializable representation of the pull cursor inside this object.
|
||||
#
|
||||
# @return {String} an ASCII string that can be passed to pullChanges instead
|
||||
# of this PulledChanges instance
|
||||
cursor: -> @cursorTag
|
||||
|
||||
# Creates a new Dropbox.PulledChanges instance from a /delta API call result.
|
||||
#
|
||||
# @private
|
||||
# This constructor is used by Dropbox.PulledChanges, and should not be called
|
||||
# directly.
|
||||
#
|
||||
# @param {Object} deltaInfo the parsed JSON of a /delta API call result
|
||||
constructor: (deltaInfo) ->
|
||||
@blankSlate = deltaInfo.reset or false
|
||||
@cursorTag = deltaInfo.cursor
|
||||
@shouldPullAgain = deltaInfo.has_more
|
||||
@shouldBackOff = not @shouldPullAgain
|
||||
if deltaInfo.cursor and deltaInfo.cursor.length
|
||||
@changes = (Dropbox.PullChange.parse entry for entry in deltaInfo.entries)
|
||||
else
|
||||
@changes = []
|
||||
|
||||
# Wraps a single change in a pullChanges result.
|
||||
class Dropbox.PullChange
|
||||
# Creates a Dropbox.PullChange instance wrapping an entry in a /delta result.
|
||||
#
|
||||
# @param {?Object} entry the parsed JSON of a single entry in a /delta API
|
||||
# call result
|
||||
# @return {?Dropbox.PullChange} a Dropbox.PullChange instance wrapping the
|
||||
# given entry of a /delta API call; if the parameter does not look like
|
||||
# parsed JSON, it is returned as is
|
||||
@parse: (entry) ->
|
||||
if entry and typeof entry is 'object'
|
||||
new Dropbox.PullChange entry
|
||||
else
|
||||
entry
|
||||
|
||||
# @property {String} the path of the changed file or folder
|
||||
path: undefined
|
||||
|
||||
# @property {Boolean} if true, this change is a deletion of the file or folder
|
||||
# at the change's path; if a folder is deleted, all its contents (files
|
||||
# and sub-folders) were also be deleted; pullChanges might not return
|
||||
# separate changes expressing for the files or sub-folders
|
||||
wasRemoved: undefined
|
||||
|
||||
# @property {?Dropbox.Stat} a Stat instance containing updated information for
|
||||
# the file or folder; this is null if the change is a deletion
|
||||
stat: undefined
|
||||
|
||||
# Creates a Dropbox.PullChange instance wrapping an entry in a /delta result.
|
||||
#
|
||||
# @private
|
||||
# This constructor is used by Dropbox.PullChange.parse, and should not be
|
||||
# called directly.
|
||||
#
|
||||
# @param {Object} entry the parsed JSON of a single entry in a /delta API
|
||||
# call result
|
||||
constructor: (entry) ->
|
||||
@path = entry[0]
|
||||
@stat = Dropbox.Stat.parse entry[1]
|
||||
if @stat
|
||||
@wasRemoved = false
|
||||
else
|
||||
@stat = null
|
||||
@wasRemoved = true
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
# Wraps an URL to a Dropbox file or folder that can be publicly shared.
|
||||
class Dropbox.PublicUrl
|
||||
# Creates a PublicUrl instance from a raw API response.
|
||||
#
|
||||
# @param {?Object, ?String} urlData the parsed JSON describing a public URL
|
||||
# @param {?Boolean} isDirect true if this is a direct download link, false if
|
||||
# is a file / folder preview link
|
||||
# @return {?Dropbox.PublicUrl} a PublicUrl instance wrapping the given public
|
||||
# link info; parameters that don't look like parsed JSON are returned as
|
||||
# they are
|
||||
@parse: (urlData, isDirect) ->
|
||||
if urlData and typeof urlData is 'object'
|
||||
new Dropbox.PublicUrl urlData, isDirect
|
||||
else
|
||||
urlData
|
||||
|
||||
# @property {String} the public URL
|
||||
url: null
|
||||
|
||||
# @property {Date} after this time, the URL is not usable
|
||||
expiresAt: null
|
||||
|
||||
# @property {Boolean} true if this is a direct download URL, false for URLs to
|
||||
# preview pages in the Dropbox web app; folders do not have direct link
|
||||
#
|
||||
isDirect: null
|
||||
|
||||
# @property {Boolean} true if this is URL points to a file's preview page in
|
||||
# Dropbox, false for direct links
|
||||
isPreview: null
|
||||
|
||||
# JSON representation of this file / folder's metadata
|
||||
#
|
||||
# @return {Object} conforms to the JSON restrictions; can be passed to
|
||||
# Dropbox.PublicUrl#parse to obtain an identical PublicUrl instance
|
||||
json: ->
|
||||
# HACK: this can break if the Dropbox API ever decides to use 'direct' in
|
||||
# its link info
|
||||
@_json ||= url: @url, expires: @expiresAt.toString(), direct: @isDirect
|
||||
|
||||
# Creates a PublicUrl instance from a raw API response.
|
||||
#
|
||||
# @private
|
||||
# This constructor is used by Dropbox.PublicUrl.parse, and should not be
|
||||
# called directly.
|
||||
#
|
||||
# @param {?Object} urlData the parsed JSON describing a public URL
|
||||
# @param {Boolean} isDirect true if this is a direct download link, false if
|
||||
# is a file / folder preview link
|
||||
constructor: (urlData, isDirect) ->
|
||||
@url = urlData.url
|
||||
@expiresAt = new Date Date.parse(urlData.expires)
|
||||
|
||||
if isDirect is true
|
||||
@isDirect = true
|
||||
else if isDirect is false
|
||||
@isDirect = false
|
||||
else
|
||||
# HACK: this can break if the Dropbox API ever decides to use 'direct' in
|
||||
# its link info; unfortunately, there's no elegant way to guess
|
||||
# between direct download URLs and preview URLs
|
||||
if 'direct' of urlData
|
||||
@isDirect = urlData.direct
|
||||
else
|
||||
@isDirect = Date.now() - @expiresAt <= 86400000 # 1 day
|
||||
@isPreview = !@isDirect
|
||||
|
||||
# The JSON representation is created on-demand, to avoid unnecessary object
|
||||
# creation.
|
||||
# We can't use the original JSON object because we add a 'direct' field.
|
||||
@_json = null
|
||||
|
||||
# Reference to a file that can be used to make a copy across users' Dropboxes.
|
||||
class Dropbox.CopyReference
|
||||
# Creates a CopyReference instance from a raw reference or API response.
|
||||
#
|
||||
# @param {?Object, ?String} refData the parsed JSON describing a copy
|
||||
# reference, or the reference string
|
||||
@parse: (refData) ->
|
||||
if refData and (typeof refData is 'object' or typeof refData is 'string')
|
||||
new Dropbox.CopyReference refData
|
||||
else
|
||||
refData
|
||||
|
||||
# @property {String} the raw reference, for use with Dropbox APIs
|
||||
tag: null
|
||||
|
||||
# @property {Date} deadline for using the reference in a copy operation
|
||||
expiresAt: null
|
||||
|
||||
# JSON representation of this file / folder's metadata
|
||||
#
|
||||
# @return {Object} conforms to the JSON restrictions; can be passed to
|
||||
# Dropbox.CopyReference#parse to obtain an identical CopyReference instance
|
||||
json: ->
|
||||
# NOTE: the assignment only occurs if the CopyReference was built around a
|
||||
# string; CopyReferences parsed from API responses hold onto the
|
||||
# original JSON
|
||||
@_json ||= copy_ref: @tag, expires: @expiresAt.toString()
|
||||
|
||||
# Creates a CopyReference instance from a raw reference or API response.
|
||||
#
|
||||
# @private
|
||||
# This constructor is used by Dropbox.CopyReference.parse, and should not be
|
||||
# called directly.
|
||||
#
|
||||
# @param {Object, String} refData the parsed JSON describing a copy
|
||||
# reference, or the reference string
|
||||
constructor: (refData) ->
|
||||
if typeof refData is 'object'
|
||||
@tag = refData.copy_ref
|
||||
@expiresAt = new Date Date.parse(refData.expires)
|
||||
@_json = refData
|
||||
else
|
||||
@tag = refData
|
||||
@expiresAt = new Date Math.ceil(Date.now() / 1000) * 1000
|
||||
# The JSON representation is created on-demand, to avoid unnecessary
|
||||
# object creation.
|
||||
@_json = null
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
# The result of stat-ing a file or directory in a user's Dropbox.
|
||||
class Dropbox.Stat
|
||||
# Creates a Stat instance from a raw "metadata" response.
|
||||
#
|
||||
# @param {?Object} metadata the result of parsing JSON API responses that are
|
||||
# called "metadata" in the API documentation
|
||||
# @return {?Dropbox.Stat} a Stat instance wrapping the given API response;
|
||||
# parameters that aren't parsed JSON objects are returned as they are
|
||||
@parse: (metadata) ->
|
||||
if metadata and typeof metadata is 'object'
|
||||
new Dropbox.Stat metadata
|
||||
else
|
||||
metadata
|
||||
|
||||
# @property {String} the path of this file or folder, relative to the user's
|
||||
# Dropbox or to the application's folder
|
||||
path: null
|
||||
|
||||
# @property {String} the name of this file or folder
|
||||
name: null
|
||||
|
||||
# @property {Boolean} if true, the file or folder's path is relative to the
|
||||
# application's folder; otherwise, the path is relative to the user's
|
||||
# Dropbox
|
||||
inAppFolder: null
|
||||
|
||||
# @property {Boolean} if true, this Stat instance describes a folder
|
||||
isFolder: null
|
||||
|
||||
# @property {Boolean} if true, this Stat instance describes a file
|
||||
isFile: null
|
||||
|
||||
# @property {Boolean} if true, the file or folder described by this Stat
|
||||
# instance was from the user's Dropbox, and was obtained by an API call
|
||||
# that returns deleted items
|
||||
isRemoved: null
|
||||
|
||||
# @property {String} name of an icon in Dropbox's icon library that most
|
||||
# accurately represents this file or folder
|
||||
#
|
||||
# See the Dropbox API documentation to obtain the Dropbox icon library.
|
||||
# https://www.dropbox.com/developers/reference/api#metadata
|
||||
typeIcon: null
|
||||
|
||||
# @property {String} an identifier for the contents of the described file or
|
||||
# directories; this can used to be restored a file's contents to a
|
||||
# previous version, or to save bandwidth by not retrieving the same
|
||||
# folder contents twice
|
||||
versionTag: null
|
||||
|
||||
# @property {String} a guess of the MIME type representing the file or
|
||||
# folder's contents
|
||||
mimeType: null
|
||||
|
||||
# @property {Number} the size of the file, in bytes; null for folders
|
||||
size: null
|
||||
|
||||
# @property {String} the size of the file, in a human-readable format, such
|
||||
# as "225.4KB"; the format of this string is influenced by the API client's
|
||||
# locale
|
||||
humanSize: null
|
||||
|
||||
# @property {Boolean} if false, the URL generated by thumbnailUrl does not
|
||||
# point to a valid image, and should not be used
|
||||
hasThumbnail: null
|
||||
|
||||
# @property {Date} the file or folder's last modification time
|
||||
modifiedAt: null
|
||||
|
||||
# @property {?Date} the file or folder's last modification time, as reported
|
||||
# by the Dropbox client that uploaded the file; this time should not be
|
||||
# trusted, but can be used for UI (display, sorting); null if the server
|
||||
# does not report any time
|
||||
clientModifiedAt: null
|
||||
|
||||
# JSON representation of this file / folder's metadata
|
||||
#
|
||||
# @return {Object} conforms to the JSON restrictions; can be passed to
|
||||
# Dropbox.Stat#parse to obtain an identical Stat instance
|
||||
json: ->
|
||||
@_json
|
||||
|
||||
# Creates a Stat instance from a raw "metadata" response.
|
||||
#
|
||||
# @private
|
||||
# This constructor is used by Dropbox.Stat.parse, and should not be called
|
||||
# directly.
|
||||
#
|
||||
# @param {Object} metadata the result of parsing JSON API responses that are
|
||||
# called "metadata" in the API documentation
|
||||
constructor: (metadata) ->
|
||||
@_json = metadata
|
||||
@path = metadata.path
|
||||
# Ensure there is a trailing /, to make path processing reliable.
|
||||
@path = '/' + @path if @path.substring(0, 1) isnt '/'
|
||||
# Strip any trailing /, to make path joining predictable.
|
||||
lastIndex = @path.length - 1
|
||||
if lastIndex >= 0 and @path.substring(lastIndex) is '/'
|
||||
@path = @path.substring 0, lastIndex
|
||||
|
||||
nameSlash = @path.lastIndexOf '/'
|
||||
@name = @path.substring nameSlash + 1
|
||||
|
||||
@isFolder = metadata.is_dir || false
|
||||
@isFile = !@isFolder
|
||||
@isRemoved = metadata.is_deleted || false
|
||||
@typeIcon = metadata.icon
|
||||
if metadata.modified?.length
|
||||
@modifiedAt = new Date Date.parse(metadata.modified)
|
||||
else
|
||||
@modifiedAt = null
|
||||
if metadata.client_mtime?.length
|
||||
@clientModifiedAt = new Date Date.parse(metadata.client_mtime)
|
||||
else
|
||||
@clientModifiedAt = null
|
||||
|
||||
switch metadata.root
|
||||
when 'dropbox'
|
||||
@inAppFolder = false
|
||||
when 'app_folder'
|
||||
@inAppFolder = true
|
||||
else
|
||||
# New "root" value that we're not aware of.
|
||||
@inAppFolder = null
|
||||
|
||||
@size = metadata.bytes or 0
|
||||
@humanSize = metadata.size or ''
|
||||
@hasThumbnail = metadata.thumb_exists or false
|
||||
|
||||
if @isFolder
|
||||
@versionTag = metadata.hash
|
||||
@mimeType = metadata.mime_type || 'inode/directory'
|
||||
else
|
||||
@versionTag = metadata.rev
|
||||
@mimeType = metadata.mime_type || 'application/octet-stream'
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# Tracks the progress of a resumable upload.
|
||||
class Dropbox.UploadCursor
|
||||
# Creates an UploadCursor instance from an API response.
|
||||
#
|
||||
# @param {?Object, ?String} cursorData the parsed JSON describing the status
|
||||
# of a partial upload, or the upload ID string
|
||||
@parse: (cursorData) ->
|
||||
if cursorData and (typeof cursorData is 'object' or
|
||||
typeof cursorData is 'string')
|
||||
new Dropbox.UploadCursor cursorData
|
||||
else
|
||||
cursorData
|
||||
|
||||
# @property {String} the server-generated ID for this upload
|
||||
tag: null
|
||||
|
||||
# @property {Number} number of bytes that have already been uploaded
|
||||
offset: null
|
||||
|
||||
# @property {Date} deadline for finishing the upload
|
||||
expiresAt: null
|
||||
|
||||
# JSON representation of this cursor.
|
||||
#
|
||||
# @return {Object} conforms to the JSON restrictions; can be passed to
|
||||
# Dropbox.UploadCursor#parse to obtain an identical UploadCursor instance
|
||||
json: ->
|
||||
# NOTE: the assignment only occurs if
|
||||
@_json ||= upload_id: @tag, offset: @offset, expires: @expiresAt.toString()
|
||||
|
||||
# Creates an UploadCursor instance from a raw reference or API response.
|
||||
#
|
||||
# This constructor should only be called directly to obtain a cursor for a
|
||||
# new file upload. Dropbox.UploadCursor#parse should be called instead
|
||||
#
|
||||
# @param {?Object, ?String} cursorData the parsed JSON describing a copy
|
||||
# reference, or the reference string
|
||||
constructor: (cursorData) ->
|
||||
@replace cursorData
|
||||
|
||||
# Replaces the current
|
||||
#
|
||||
# @private Called by Dropbox.Client#resumableUploadStep.
|
||||
#
|
||||
# @param {?Object, ?String} cursorData the parsed JSON describing a copy
|
||||
# reference, or the reference string
|
||||
# @return {Dropbox.UploadCursor} this
|
||||
replace: (cursorData) ->
|
||||
if typeof cursorData is 'object'
|
||||
@tag = cursorData.upload_id or null
|
||||
@offset = cursorData.offset or 0
|
||||
@expiresAt = new Date(Date.parse(cursorData.expires) or Date.now())
|
||||
@_json = cursorData
|
||||
else
|
||||
@tag = cursorData or null
|
||||
@offset = 0
|
||||
@expiresAt = new Date Math.floor(Date.now() / 1000) * 1000
|
||||
@_json = null
|
||||
@
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Information about a Dropbox user.
|
||||
class Dropbox.UserInfo
|
||||
# Creates a UserInfo instance from a raw API response.
|
||||
#
|
||||
# @param {?Object} userInfo the result of parsing a JSON API response that
|
||||
# describes a user
|
||||
# @return {Dropbox.UserInfo} a UserInfo instance wrapping the given API
|
||||
# response; parameters that aren't parsed JSON objects are returned as
|
||||
# the are
|
||||
@parse: (userInfo) ->
|
||||
if userInfo and typeof userInfo is 'object'
|
||||
new Dropbox.UserInfo userInfo
|
||||
else
|
||||
userInfo
|
||||
|
||||
# @property {String} the user's name, in a form that is fit for display
|
||||
name: null
|
||||
|
||||
# @property {?String} the user's email; this is not in the official API
|
||||
# documentation, so it might not be supported
|
||||
email: null
|
||||
|
||||
# @property {?String} two-letter country code, or null if unavailable
|
||||
countryCode: null
|
||||
|
||||
# @property {String} unique ID for the user; this ID matches the unique ID
|
||||
# returned by the authentication process
|
||||
uid: null
|
||||
|
||||
# @property {String} the user's referral link; the user might benefit if
|
||||
# others use the link to sign up for Dropbox
|
||||
referralUrl: null
|
||||
|
||||
# Specific to applications whose access type is "public app folder".
|
||||
#
|
||||
# @property {String} prefix for URLs to the application's files
|
||||
publicAppUrl: null
|
||||
|
||||
# @property {Number} the maximum amount of bytes that the user can store
|
||||
quota: null
|
||||
|
||||
# @property {Number} the number of bytes taken up by the user's data
|
||||
usedQuota: null
|
||||
|
||||
# @property {Number} the number of bytes taken up by the user's data that is
|
||||
# not shared with other users
|
||||
privateBytes: null
|
||||
|
||||
# @property {Number} the number of bytes taken up by the user's data that is
|
||||
# shared with other users
|
||||
sharedBytes: null
|
||||
|
||||
# JSON representation of this user's information.
|
||||
#
|
||||
# @return {Object} conforms to the JSON restrictions; can be passed to
|
||||
# Dropbox.UserInfo#parse to obtain an identical UserInfo instance
|
||||
json: ->
|
||||
@_json
|
||||
|
||||
# Creates a UserInfo instance from a raw API response.
|
||||
#
|
||||
# @private
|
||||
# This constructor is used by Dropbox.UserInfo.parse, and should not be
|
||||
# called directly.
|
||||
#
|
||||
# @param {Object} userInfo the result of parsing a JSON API response that
|
||||
# describes a user
|
||||
constructor: (userInfo) ->
|
||||
@_json = userInfo
|
||||
@name = userInfo.display_name
|
||||
@email = userInfo.email
|
||||
@countryCode = userInfo.country or null
|
||||
@uid = userInfo.uid.toString()
|
||||
if userInfo.public_app_url
|
||||
@publicAppUrl = userInfo.public_app_url
|
||||
lastIndex = @publicAppUrl.length - 1
|
||||
# Strip any trailing /, to make path joining predictable.
|
||||
if lastIndex >= 0 and @publicAppUrl.substring(lastIndex) is '/'
|
||||
@publicAppUrl = @publicAppUrl.substring 0, lastIndex
|
||||
else
|
||||
@publicAppUrl = null
|
||||
|
||||
@referralUrl = userInfo.referral_link
|
||||
@quota = userInfo.quota_info.quota
|
||||
@privateBytes = userInfo.quota_info.normal or 0
|
||||
@sharedBytes = userInfo.quota_info.shared or 0
|
||||
@usedQuota = @privateBytes + @sharedBytes
|
||||
|
||||
|
|
@ -1,505 +0,0 @@
|
|||
if window?
|
||||
if window.XDomainRequest and not ('withCredentials' of new XMLHttpRequest())
|
||||
DropboxXhrRequest = window.XDomainRequest
|
||||
DropboxXhrIeMode = true
|
||||
# IE's XDR doesn't allow setting requests' Content-Type to anything other
|
||||
# than text/plain, so it can't send _any_ forms.
|
||||
DropboxXhrCanSendForms = false
|
||||
else
|
||||
DropboxXhrRequest = window.XMLHttpRequest
|
||||
DropboxXhrIeMode = false
|
||||
# Firefox doesn't support adding named files to FormData.
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=690659
|
||||
DropboxXhrCanSendForms =
|
||||
window.navigator.userAgent.indexOf('Firefox') is -1
|
||||
DropboxXhrDoesPreflight = true
|
||||
else
|
||||
# Node.js needs an adapter for the XHR API.
|
||||
DropboxXhrRequest = require('xmlhttprequest').XMLHttpRequest
|
||||
DropboxXhrIeMode = false
|
||||
# Node.js doesn't have FormData. We wouldn't want to bother putting together
|
||||
# upload forms in node.js anyway, because it doesn't do CORS preflight
|
||||
# checks, so we can use PUT requests without a performance hit.
|
||||
DropboxXhrCanSendForms = false
|
||||
# Node.js is a server so it doesn't do annoying browser checks.
|
||||
DropboxXhrDoesPreflight = false
|
||||
|
||||
# ArrayBufferView isn't available in the global namespce.
|
||||
#
|
||||
# Using the hack suggested in
|
||||
# https://code.google.com/p/chromium/issues/detail?id=60449
|
||||
if typeof Uint8Array is 'undefined'
|
||||
DropboxXhrArrayBufferView = null
|
||||
DropboxXhrSendArrayBufferView = false
|
||||
else
|
||||
if Object.getPrototypeOf
|
||||
DropboxXhrArrayBufferView = Object.getPrototypeOf(
|
||||
Object.getPrototypeOf(new Uint8Array(0))).constructor
|
||||
else if Object.__proto__
|
||||
DropboxXhrArrayBufferView =
|
||||
(new Uint8Array(0)).__proto__.__proto__.constructor
|
||||
|
||||
# Browsers that haven't implemented XHR#send(ArrayBufferView) also don't
|
||||
# have a real ArrayBufferView prototype. (Safari, Firefox)
|
||||
DropboxXhrSendArrayBufferView = DropboxXhrArrayBufferView isnt Object
|
||||
|
||||
# Dispatches low-level AJAX calls (XMLHttpRequests).
|
||||
class Dropbox.Xhr
|
||||
# The object used to perform AJAX requests (XMLHttpRequest).
|
||||
@Request = DropboxXhrRequest
|
||||
# Set to true when using the XDomainRequest API.
|
||||
@ieXdr = DropboxXhrIeMode
|
||||
# Set to true if the platform has proper support for FormData.
|
||||
@canSendForms = DropboxXhrCanSendForms
|
||||
# Set to true if the platform performs CORS preflight checks.
|
||||
@doesPreflight = DropboxXhrDoesPreflight
|
||||
# Superclass for all ArrayBufferView objects.
|
||||
@ArrayBufferView = DropboxXhrArrayBufferView
|
||||
# Set to true if we think we can send ArrayBufferView objects via XHR.
|
||||
@sendArrayBufferView = DropboxXhrSendArrayBufferView
|
||||
|
||||
|
||||
# Sets up an AJAX request.
|
||||
#
|
||||
# @param {String} method the HTTP method used to make the request ('GET',
|
||||
# 'POST', 'PUT', etc.)
|
||||
# @param {String} baseUrl the URL that receives the request; this URL might
|
||||
# be modified, e.g. by appending parameters for GET requests
|
||||
constructor: (@method, baseUrl) ->
|
||||
@isGet = @method is 'GET'
|
||||
@url = baseUrl
|
||||
@headers = {}
|
||||
@params = null
|
||||
@body = null
|
||||
@preflight = not (@isGet or (@method is 'POST'))
|
||||
@signed = false
|
||||
@responseType = null
|
||||
@callback = null
|
||||
@xhr = null
|
||||
@onError = null
|
||||
|
||||
# @property {?XMLHttpRequest} the raw XMLHttpRequest object used to make the
|
||||
# request; null until Dropbox.Xhr#prepare is called
|
||||
xhr: null
|
||||
|
||||
# @property {?Dropbox.EventSource<Dropbox.ApiError>} if the XHR fails and
|
||||
# this property is set, the Dropbox.ApiError instance that will be passed
|
||||
# to the callback will be dispatched through the Dropbox.EventSource; the
|
||||
# EventSource should be configured for non-cancelable events
|
||||
onError: null
|
||||
|
||||
# Sets the parameters (form field values) that will be sent with the request.
|
||||
#
|
||||
# @param {?Object} params an associative array (hash) containing the HTTP
|
||||
# request parameters
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
setParams: (params) ->
|
||||
if @signed
|
||||
throw new Error 'setParams called after addOauthParams or addOauthHeader'
|
||||
if @params
|
||||
throw new Error 'setParams cannot be called twice'
|
||||
@params = params
|
||||
@
|
||||
|
||||
# Sets the function called when the XHR completes.
|
||||
#
|
||||
# This function can also be set when calling Dropbox.Xhr#send.
|
||||
#
|
||||
# @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when
|
||||
# the XHR completes; if an error occurs, the first parameter will be a
|
||||
# Dropbox.ApiError instance; otherwise, the second parameter will be an
|
||||
# instance of the required response type (e.g., String, Blob), and the
|
||||
# third parameter will be the JSON-parsed 'x-dropbox-metadata' header
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
setCallback: (@callback) ->
|
||||
@
|
||||
|
||||
# Ammends the request parameters to include an OAuth signature.
|
||||
#
|
||||
# The OAuth signature will become invalid if the parameters are changed after
|
||||
# the signing process.
|
||||
#
|
||||
# This method automatically decides the best way to add the OAuth signature
|
||||
# to the current request. Modifying the request in any way (e.g., by adding
|
||||
# headers) might result in a valid signature that is applied in a sub-optimal
|
||||
# fashion. For best results, call this right before Dropbox.Xhr#prepare.
|
||||
#
|
||||
# @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
|
||||
# used to sign the request
|
||||
# @param {Boolean} cacheFriendly if true, the signing process choice will be
|
||||
# biased towards allowing the HTTP cache to work; by default, the choice
|
||||
# attempts to avoid the CORS preflight request whenever possible
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
signWithOauth: (oauth, cacheFriendly) ->
|
||||
if Dropbox.Xhr.ieXdr
|
||||
@addOauthParams oauth
|
||||
else if @preflight or !Dropbox.Xhr.doesPreflight
|
||||
@addOauthHeader oauth
|
||||
else
|
||||
if @isGet and cacheFriendly
|
||||
@addOauthHeader oauth
|
||||
else
|
||||
@addOauthParams oauth
|
||||
|
||||
# Ammends the request parameters to include an OAuth signature.
|
||||
#
|
||||
# The OAuth signature will become invalid if the parameters are changed after
|
||||
# the signing process.
|
||||
#
|
||||
# @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
|
||||
# used to sign the request
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
addOauthParams: (oauth) ->
|
||||
if @signed
|
||||
throw new Error 'Request already has an OAuth signature'
|
||||
|
||||
@params or= {}
|
||||
oauth.addAuthParams @method, @url, @params
|
||||
@signed = true
|
||||
@
|
||||
|
||||
# Adds an Authorize header containing an OAuth signature.
|
||||
#
|
||||
# The OAuth signature will become invalid if the parameters are changed after
|
||||
# the signing process.
|
||||
#
|
||||
# @param {Dropbox.Oauth} oauth OAuth instance whose key and secret will be
|
||||
# used to sign the request
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
addOauthHeader: (oauth) ->
|
||||
if @signed
|
||||
throw new Error 'Request already has an OAuth signature'
|
||||
|
||||
@params or= {}
|
||||
@signed = true
|
||||
@setHeader 'Authorization', oauth.authHeader(@method, @url, @params)
|
||||
|
||||
# Sets the body (piece of data) that will be sent with the request.
|
||||
#
|
||||
# @param {String, Blob, ArrayBuffer} body the body to be sent in a request;
|
||||
# GET requests cannot have a body
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
setBody: (body) ->
|
||||
if @isGet
|
||||
throw new Error 'setBody cannot be called on GET requests'
|
||||
if @body isnt null
|
||||
throw new Error 'Request already has a body'
|
||||
|
||||
if typeof body is 'string'
|
||||
# Content-Type will be set automatically.
|
||||
else if (typeof FormData isnt 'undefined') and (body instanceof FormData)
|
||||
# Content-Type will be set automatically.
|
||||
else
|
||||
@headers['Content-Type'] = 'application/octet-stream'
|
||||
@preflight = true
|
||||
|
||||
@body = body
|
||||
@
|
||||
|
||||
# Sends off an AJAX request and requests a custom response type.
|
||||
#
|
||||
# This method requires XHR Level 2 support, which is not available in IE
|
||||
# versions <= 9. If these browsers must be supported, it is recommended to
|
||||
# check whether window.Blob is truthy.
|
||||
#
|
||||
# @param {String} responseType the value that will be assigned to the XHR's
|
||||
# responseType property
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
setResponseType: (@responseType) ->
|
||||
@
|
||||
|
||||
# Sets the value of a custom HTTP header.
|
||||
#
|
||||
# Custom HTTP headers require a CORS preflight in browsers, so requests that
|
||||
# use them will take more time to complete, especially on high-latency mobile
|
||||
# connections.
|
||||
#
|
||||
# @param {String} headerName the name of the HTTP header
|
||||
# @param {String} value the value that the header will be set to
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
setHeader: (headerName, value) ->
|
||||
if @headers[headerName]
|
||||
oldValue = @headers[headerName]
|
||||
throw new Error "HTTP header #{headerName} already set to #{oldValue}"
|
||||
if headerName is 'Content-Type'
|
||||
throw new Error 'Content-Type is automatically computed based on setBody'
|
||||
@preflight = true
|
||||
@headers[headerName] = value
|
||||
@
|
||||
|
||||
# Simulates having an <input type="file"> being sent with the request.
|
||||
#
|
||||
# @param {String} fieldName the name of the form field / parameter (not of
|
||||
# the uploaded file)
|
||||
# @param {String} fileName the name of the uploaded file (not the name of the
|
||||
# form field / parameter)
|
||||
# @param {String, Blob, File} fileData contents of the file to be uploaded
|
||||
# @param {?String} contentType the MIME type of the file to be uploaded; if
|
||||
# fileData is a Blob or File, its MIME type is used instead
|
||||
setFileField: (fieldName, fileName, fileData, contentType) ->
|
||||
if @body isnt null
|
||||
throw new Error 'Request already has a body'
|
||||
|
||||
if @isGet
|
||||
throw new Error 'setFileField cannot be called on GET requests'
|
||||
|
||||
if typeof(fileData) is 'object' and typeof Blob isnt 'undefined'
|
||||
if typeof ArrayBuffer isnt 'undefined'
|
||||
if fileData instanceof ArrayBuffer
|
||||
# Convert ArrayBuffer -> ArrayBufferView on standard-compliant
|
||||
# browsers, to avoid warnings from the Blob constructor.
|
||||
if Dropbox.Xhr.sendArrayBufferView
|
||||
fileData = new Uint8Array fileData
|
||||
else
|
||||
# Convert ArrayBufferView -> ArrayBuffer on older browsers, to avoid
|
||||
# having a Blob that contains "[object Uint8Array]" instead of the
|
||||
# actual data.
|
||||
if !Dropbox.Xhr.sendArrayBufferView and fileData.byteOffset is 0 and
|
||||
fileData.buffer instanceof ArrayBuffer
|
||||
fileData = fileData.buffer
|
||||
|
||||
contentType or= 'application/octet-stream'
|
||||
fileData = new Blob [fileData], type: contentType
|
||||
|
||||
# Workaround for http://crbug.com/165095
|
||||
if typeof File isnt 'undefined' and fileData instanceof File
|
||||
fileData = new Blob [fileData], type: fileData.type
|
||||
#fileData = fileData
|
||||
useFormData = fileData instanceof Blob
|
||||
else
|
||||
useFormData = false
|
||||
|
||||
if useFormData
|
||||
@body = new FormData()
|
||||
@body.append fieldName, fileData, fileName
|
||||
else
|
||||
contentType or= 'application/octet-stream'
|
||||
boundary = @multipartBoundary()
|
||||
@headers['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
||||
@body = ['--', boundary, "\r\n",
|
||||
'Content-Disposition: form-data; name="', fieldName,
|
||||
'"; filename="', fileName, "\"\r\n",
|
||||
'Content-Type: ', contentType, "\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n\r\n",
|
||||
fileData,
|
||||
"\r\n", '--', boundary, '--', "\r\n"].join ''
|
||||
|
||||
# @private
|
||||
# @return {String} a nonce suitable for use as a part boundary in a multipart
|
||||
# MIME message
|
||||
multipartBoundary: ->
|
||||
[Date.now().toString(36), Math.random().toString(36)].join '----'
|
||||
|
||||
# Moves this request's parameters to its URL.
|
||||
#
|
||||
# @private
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
paramsToUrl: ->
|
||||
if @params
|
||||
queryString = Dropbox.Xhr.urlEncode @params
|
||||
if queryString.length isnt 0
|
||||
@url = [@url, '?', queryString].join ''
|
||||
@params = null
|
||||
@
|
||||
|
||||
# Moves this request's parameters to its body.
|
||||
#
|
||||
# @private
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
paramsToBody: ->
|
||||
if @params
|
||||
if @body isnt null
|
||||
throw new Error 'Request already has a body'
|
||||
if @isGet
|
||||
throw new Error 'paramsToBody cannot be called on GET requests'
|
||||
@headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||
@body = Dropbox.Xhr.urlEncode @params
|
||||
@params = null
|
||||
@
|
||||
|
||||
# Sets up an XHR request.
|
||||
#
|
||||
# This method completely sets up a native XHR object and stops short of
|
||||
# calling its send() method, so the API client has a chance of customizing
|
||||
# the XHR. After customizing the XHR, Dropbox.Xhr#send should be called.
|
||||
#
|
||||
#
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
prepare: ->
|
||||
ieXdr = Dropbox.Xhr.ieXdr
|
||||
if @isGet or @body isnt null or ieXdr
|
||||
@paramsToUrl()
|
||||
if @body isnt null and typeof @body is 'string'
|
||||
@headers['Content-Type'] = 'text/plain; charset=utf8'
|
||||
else
|
||||
@paramsToBody()
|
||||
|
||||
@xhr = new Dropbox.Xhr.Request()
|
||||
if ieXdr
|
||||
@xhr.onload = => @onXdrLoad()
|
||||
@xhr.onerror = => @onXdrError()
|
||||
@xhr.ontimeout = => @onXdrError()
|
||||
# NOTE: there are reports that XHR somtimes fails if onprogress doesn't
|
||||
# have any handler
|
||||
@xhr.onprogress = ->
|
||||
else
|
||||
@xhr.onreadystatechange = => @onReadyStateChange()
|
||||
@xhr.open @method, @url, true
|
||||
|
||||
unless ieXdr
|
||||
for own header, value of @headers
|
||||
@xhr.setRequestHeader header, value
|
||||
|
||||
if @responseType
|
||||
if @responseType is 'b'
|
||||
if @xhr.overrideMimeType
|
||||
@xhr.overrideMimeType 'text/plain; charset=x-user-defined'
|
||||
else
|
||||
@xhr.responseType = @responseType
|
||||
|
||||
@
|
||||
|
||||
# Fires off the prepared XHR request.
|
||||
#
|
||||
# Dropbox.Xhr#prepare should be called exactly once before this method.
|
||||
#
|
||||
# @param {function(?Dropbox.ApiError, ?Object, ?Object)} callback called when
|
||||
# the XHR completes; if an error occurs, the first parameter will be a
|
||||
# Dropbox.ApiError instance; otherwise, the second parameter will be an
|
||||
# instance of the required response type (e.g., String, Blob), and the
|
||||
# third parameter will be the JSON-parsed 'x-dropbox-metadata' header
|
||||
# @return {Dropbox.Xhr} this, for easy call chaining
|
||||
send: (callback) ->
|
||||
@callback = callback or @callback
|
||||
|
||||
if @body isnt null
|
||||
body = @body
|
||||
# send() in XHR doesn't like naked ArrayBuffers
|
||||
if Dropbox.Xhr.sendArrayBufferView and body instanceof ArrayBuffer
|
||||
body = new Uint8Array body
|
||||
|
||||
try
|
||||
@xhr.send body
|
||||
catch e
|
||||
# Node.js doesn't implement Blob.
|
||||
if !Dropbox.Xhr.sendArrayBufferView and typeof Blob isnt 'undefined'
|
||||
# Firefox doesn't support sending ArrayBufferViews.
|
||||
body = new Blob [body], type: 'application/octet-stream'
|
||||
@xhr.send body
|
||||
else
|
||||
throw e
|
||||
else
|
||||
@xhr.send()
|
||||
@
|
||||
|
||||
# Encodes an associative array (hash) into a x-www-form-urlencoded String.
|
||||
#
|
||||
# For consistency, the keys are sorted in alphabetical order in the encoded
|
||||
# output.
|
||||
#
|
||||
# @param {Object} object the JavaScript object whose keys will be encoded
|
||||
# @return {String} the object's keys and values, encoded using
|
||||
# x-www-form-urlencoded
|
||||
@urlEncode: (object) ->
|
||||
chunks = []
|
||||
for key, value of object
|
||||
chunks.push @urlEncodeValue(key) + '=' + @urlEncodeValue(value)
|
||||
chunks.sort().join '&'
|
||||
|
||||
# Encodes an object into a x-www-form-urlencoded key or value.
|
||||
#
|
||||
# @param {Object} object the object to be encoded; the encoding calls
|
||||
# toString() on the object to obtain its string representation
|
||||
# @return {String} encoded string, suitable for use as a key or value in an
|
||||
# x-www-form-urlencoded string
|
||||
@urlEncodeValue: (object) ->
|
||||
encodeURIComponent(object.toString()).replace(/\!/g, '%21').
|
||||
replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').
|
||||
replace(/\*/g, '%2A')
|
||||
|
||||
# Decodes an x-www-form-urlencoded String into an associative array (hash).
|
||||
#
|
||||
# @param {String} string the x-www-form-urlencoded String to be decoded
|
||||
# @return {Object} an associative array whose keys and values are all strings
|
||||
@urlDecode: (string) ->
|
||||
result = {}
|
||||
for token in string.split '&'
|
||||
kvp = token.split '='
|
||||
result[decodeURIComponent(kvp[0])] = decodeURIComponent kvp[1]
|
||||
result
|
||||
|
||||
# Handles the XHR readystate event.
|
||||
onReadyStateChange: ->
|
||||
return true if @xhr.readyState isnt 4 # XMLHttpRequest.DONE is 4
|
||||
|
||||
if @xhr.status < 200 or @xhr.status >= 300
|
||||
apiError = new Dropbox.ApiError @xhr, @method, @url
|
||||
@onError.dispatch apiError if @onError
|
||||
@callback apiError
|
||||
return true
|
||||
|
||||
metadataJson = @xhr.getResponseHeader 'x-dropbox-metadata'
|
||||
if metadataJson?.length
|
||||
try
|
||||
metadata = JSON.parse metadataJson
|
||||
catch e
|
||||
# Make sure the app doesn't crash if the server goes crazy.
|
||||
metadata = undefined
|
||||
else
|
||||
metadata = undefined
|
||||
|
||||
if @responseType
|
||||
if @responseType is 'b'
|
||||
dirtyText = if @xhr.responseText?
|
||||
@xhr.responseText
|
||||
else
|
||||
@xhr.response
|
||||
###
|
||||
jsString = ['["']
|
||||
for i in [0...dirtyText.length]
|
||||
hexByte = (dirtyText.charCodeAt(i) & 0xFF).toString(16)
|
||||
if hexByte.length is 2
|
||||
jsString.push "\\u00#{hexByte}"
|
||||
else
|
||||
jsString.push "\\u000#{hexByte}"
|
||||
jsString.push '"]'
|
||||
console.log jsString
|
||||
text = JSON.parse(jsString.join(''))[0]
|
||||
###
|
||||
bytes = []
|
||||
for i in [0...dirtyText.length]
|
||||
bytes.push String.fromCharCode(dirtyText.charCodeAt(i) & 0xFF)
|
||||
text = bytes.join ''
|
||||
@callback null, text, metadata
|
||||
else
|
||||
@callback null, @xhr.response, metadata
|
||||
return true
|
||||
|
||||
text = if @xhr.responseText? then @xhr.responseText else @xhr.response
|
||||
switch @xhr.getResponseHeader('Content-Type')
|
||||
when 'application/x-www-form-urlencoded'
|
||||
@callback null, Dropbox.Xhr.urlDecode(text), metadata
|
||||
when 'application/json', 'text/javascript'
|
||||
@callback null, JSON.parse(text), metadata
|
||||
else
|
||||
@callback null, text, metadata
|
||||
true
|
||||
|
||||
# Handles the XDomainRequest onload event. (IE 8, 9)
|
||||
onXdrLoad: ->
|
||||
text = @xhr.responseText
|
||||
switch @xhr.contentType
|
||||
when 'application/x-www-form-urlencoded'
|
||||
@callback null, Dropbox.Xhr.urlDecode(text), undefined
|
||||
when 'application/json', 'text/javascript'
|
||||
@callback null, JSON.parse(text), undefined
|
||||
else
|
||||
@callback null, text, undefined
|
||||
true
|
||||
|
||||
# Handles the XDomainRequest onload event. (IE 8, 9)
|
||||
onXdrError: ->
|
||||
apiError = new Dropbox.ApiError @xhr, @method, @url
|
||||
@onError.dispatch apiError if @onError
|
||||
@callback apiError
|
||||
return true
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# All changes to the global namespace happen here.
|
||||
|
||||
# This file's name is set up in such a way that it will always show up last in
|
||||
# the source directory. This makes coffee --join work as intended.
|
||||
|
||||
if module?.exports?
|
||||
# We're a node.js module, so export the Dropbox class.
|
||||
module.exports = Dropbox
|
||||
else if window?
|
||||
# We're in a browser, so add Dropbox to the global namespace.
|
||||
window.Dropbox = Dropbox
|
||||
else
|
||||
throw new Error('This library only supports node.js and modern browsers.')
|
||||
|
||||
# These are mostly useful for testing. Clients shouldn't use internal stuff.
|
||||
Dropbox.atob = atob
|
||||
Dropbox.btoa = btoa
|
||||
Dropbox.hmac = base64HmacSha1
|
||||
Dropbox.sha1 = base64Sha1
|
||||
Dropbox.encodeKey = dropboxEncodeKey
|
||||
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100px" height="94.209px" viewBox="0 0 100 94.209" enable-background="new 0 0 100 94.209" xml:space="preserve">
|
||||
<path d="M51.62,0c10.969,0.871,19.611,10.069,19.611,21.262c0,11.397-8.966,20.693-20.221,21.272v4.367
|
||||
c3.071,0.485,5.426,3.154,5.426,6.362c0,0.784-0.145,1.536-0.4,2.232l3.992,2.305c6.17-9.318,18.615-12.359,28.422-6.696
|
||||
c9.786,5.651,13.4,17.907,8.467,27.893c6.771-13.177,2.062-29.512-10.911-37.001c-3.243-1.873-6.7-3.014-10.2-3.486
|
||||
c1.339-3.264,2.081-6.836,2.081-10.583C77.887,13.059,66.268,0.886,51.62,0L51.62,0z"/>
|
||||
<path d="M48.044,0.016c-14.575,0.963-26.119,13.095-26.119,27.91c0,3.641,0.696,7.119,1.962,10.306
|
||||
c-3.393,0.497-6.754,1.627-9.905,3.446C1.099,49.117-3.622,65.28,2.952,78.412c-4.747-9.94-1.106-22.032,8.594-27.634
|
||||
c9.87-5.698,22.402-2.581,28.53,6.876l3.853-2.223c-0.242-0.677-0.366-1.409-0.366-2.168c0-3.071,2.155-5.643,5.032-6.281v-4.457
|
||||
c-11.153-0.684-20.006-9.938-20.006-21.264C28.589,10.119,37.144,0.959,48.044,0.016L48.044,0.016z"/>
|
||||
<path d="M49.29,26.803c-5.63,0.129-10.819,2.046-15.079,5.165c0.34,0.503,0.706,0.991,1.091,1.458
|
||||
c0.386,0.468,0.794,0.916,1.222,1.344s0.876,0.836,1.344,1.222c0.343,0.284,0.712,0.539,1.075,0.799
|
||||
c3.144-2.066,6.91-3.275,10.966-3.275c4.058,0,7.813,1.209,10.957,3.275c0.363-0.26,0.731-0.515,1.075-0.799
|
||||
c0.468-0.386,0.916-0.794,1.344-1.222s0.836-0.876,1.222-1.344c0.386-0.468,0.752-0.956,1.093-1.458
|
||||
c-4.415-3.23-9.826-5.165-15.69-5.165c-0.14,0-0.278-0.002-0.418,0C49.428,26.804,49.357,26.8,49.29,26.803L49.29,26.803z"/>
|
||||
<path d="M23.628,50.297C23.035,55.735,24.068,61.39,27,66.47c2.933,5.08,7.314,8.809,12.316,11.014
|
||||
c0.269-0.548,0.508-1.11,0.717-1.678c0.214-0.57,0.398-1.143,0.555-1.728s0.292-1.18,0.391-1.777
|
||||
c0.075-0.438,0.115-0.883,0.154-1.329c-3.357-1.689-6.288-4.346-8.317-7.86C30.79,59.6,29.954,55.74,30.17,51.984
|
||||
c-0.407-0.185-0.812-0.373-1.23-0.53c-0.568-0.211-1.142-0.396-1.728-0.553c-0.585-0.158-1.177-0.282-1.776-0.383
|
||||
C24.839,50.417,24.234,50.34,23.628,50.297L23.628,50.297z"/>
|
||||
<path d="M76.368,50.615c-0.604,0.043-1.21,0.121-1.807,0.221c-0.598,0.101-1.19,0.227-1.777,0.383
|
||||
c-0.582,0.157-1.165,0.343-1.732,0.554c-0.418,0.156-0.816,0.354-1.225,0.538c0.217,3.753-0.617,7.616-2.648,11.128
|
||||
c-2.025,3.512-4.958,6.163-8.318,7.854c0.045,0.445,0.082,0.888,0.157,1.328c0.102,0.598,0.227,1.19,0.383,1.775
|
||||
c0.157,0.585,0.342,1.167,0.556,1.734c0.209,0.57,0.448,1.125,0.717,1.673c5.003-2.208,9.384-5.929,12.315-11.007
|
||||
C75.92,61.716,76.961,56.053,76.368,50.615L76.368,50.615z"/>
|
||||
<path d="M45.151,57.492l-3.878,2.248c4.985,10.003,1.401,22.296-8.408,27.959c-9.759,5.635-22.142,2.671-28.342-6.541
|
||||
c8.046,12.395,24.497,16.459,37.442,8.985c3.136-1.81,5.782-4.145,7.903-6.817c2.163,2.81,4.9,5.251,8.164,7.136
|
||||
c12.96,7.481,29.432,3.402,37.467-9.025c-6.188,9.249-18.588,12.243-28.367,6.596c-9.869-5.697-13.438-18.109-8.311-28.146
|
||||
l-4.032-2.329C53.611,58.869,51.901,59.7,50,59.7C48.068,59.7,46.331,58.841,45.151,57.492L45.151,57.492z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 428 B |
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 14.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 43363) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="100px" height="94.113px" viewBox="0 0 100 94.113" enable-background="new 0 0 100 94.113" xml:space="preserve">
|
||||
<path d="M74.868,86.582l-16.531-28.37c-3.038,1.997-8.752,2.129-8.752,2.129s-5.779-0.132-8.817-2.129l-16.58,28.37
|
||||
c0,0,9.743,7.531,25.612,7.531C65.619,94.113,74.868,86.582,74.868,86.582L74.868,86.582L74.868,86.582z"/>
|
||||
<path d="M49.881,53.471c5.417,0,9.745-4.408,9.745-9.792c0-5.417-4.328-9.827-9.745-9.827s-9.791,4.41-9.791,9.827
|
||||
C40.09,49.063,44.464,53.471,49.881,53.471L49.881,53.471L49.881,53.471z"/>
|
||||
<path d="M74.539,0.578L58.254,29.114c3.237,1.618,6.16,6.49,6.16,6.49s2.823,5.201,2.575,8.868l32.864-0.033
|
||||
c0,0,1.666-12.121-6.244-25.877C85.651,4.822,74.539,0.578,74.539,0.578L74.539,0.578L74.539,0.578z"/>
|
||||
<path d="M0.191,43.845h32.897c-0.247-3.667,2.51-8.737,2.51-8.737s2.988-4.953,6.29-6.588L25.557,0c0,0-11.378,4.658-19.32,18.397
|
||||
C-1.674,32.103,0.191,43.845,0.191,43.845L0.191,43.845L0.191,43.845z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 324 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 854 B |
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100" viewBox="0 0 100 100">
|
||||
<path d="M 66.177,0 H 50 33.823 v 3.724 h 4.445 V 88.047 C 38.269,94.638 43.531,100 50,100 56.469,100 61.731,94.638 61.731,88.047 V 3.724 h 4.445 V 0 z M 50,96.28 c -4.416,0 -8.012,-3.695 -8.012,-8.233 V 36.559 H 58.011 V 88.047 C 58.012,92.585 54.416,96.28 50,96.28 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 439 B |
|
Before Width: | Height: | Size: 1,017 B |
|
Before Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 421 B |
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"name": "dropbox.js Test Suite",
|
||||
"version": "1.0",
|
||||
"manifest_version": 2,
|
||||
"description": "Test suite for Chrome applications and extensions.",
|
||||
"icons": {
|
||||
"16": "images/icon16.png",
|
||||
"48": "images/icon48.png",
|
||||
"128": "images/icon128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
"app": {
|
||||
"launch": {
|
||||
"local_path": "test/html/browser_test.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"name": "dropbox.js Test Suite",
|
||||
"version": "1.0",
|
||||
"manifest_version": 2,
|
||||
"description": "Test suite for Chrome applications and extensions.",
|
||||
"icons": {
|
||||
"16": "images/icon16.png",
|
||||
"48": "images/icon48.png",
|
||||
"128": "images/icon128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
"app": {
|
||||
"background": {
|
||||
"scripts": [
|
||||
"test/js/chrome_app_background.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# dropbox.js Test Automator
|
||||
|
||||
This is a Google Chrome extension that fully automates the dropbox.js test
|
||||
suite. Read the
|
||||
[dropbox.js development guide](https://github.com/dropbox/dropbox-js/tree/master/doc)
|
||||
to learn how the extension fits into the testing process.
|
||||
|
||||
You're welcome to reuse the code to automate the testing of your own
|
||||
application's integration with Dropbox.
|
||||
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# Background script orchestrating the dropbox.js testing automation.
|
||||
|
||||
class Automator
|
||||
constructor: ->
|
||||
@wired = false
|
||||
chrome.storage.sync.get 'enabled', (values) =>
|
||||
@lifeSwitch values.enabled is 'true'
|
||||
|
||||
# Activates or deactivates the extension.
|
||||
# @param {Boolean} enabled if true, the extension's functionality is enabled
|
||||
lifeSwitch: (enabled) ->
|
||||
if enabled
|
||||
chrome.browserAction.setIcon
|
||||
path:
|
||||
19: 'images/action_on19.png'
|
||||
38: 'images/action_on38.png'
|
||||
chrome.browserAction.setTitle title: '(on) dropbox.js Test Automator'
|
||||
@wire()
|
||||
else
|
||||
chrome.browserAction.setIcon
|
||||
path:
|
||||
19: 'images/action_off19.png',
|
||||
38: 'images/action_off38.png'
|
||||
chrome.browserAction.setTitle title: '(off) dropbox.js Test Automator'
|
||||
@unwire()
|
||||
|
||||
# Checks if Dropbox's authorization dialog should be auto-clicked.
|
||||
# @param {String} url the URL of the Dropbox authorization dialog
|
||||
# @return {Boolean} true if the "Authorize" button should be auto-clicked
|
||||
shouldAutomateAuth: (url) ->
|
||||
return false unless @wired
|
||||
!!(/(\?|&)oauth_callback=https?%3A%2F%2Flocalhost%3A891[12]%2F/.exec(url))
|
||||
|
||||
# Checks if an OAuth receiver window should be auto-closed.
|
||||
# @param {String} url the URL of the OAuth receiver window
|
||||
# @return {Boolean} true if the "Authorize" button should be auto-clicked
|
||||
shouldAutomateClose: (url) ->
|
||||
return false unless @wired
|
||||
!!(/^https?:\/\/localhost:8912\/oauth_callback\?/.exec(url))
|
||||
|
||||
# Sets up all the features that make dropbox.js testing easier.
|
||||
wire: ->
|
||||
return if @wired
|
||||
chrome.contentSettings.popups.set(
|
||||
primaryPattern: 'http://localhost:8911/*', setting: 'allow')
|
||||
@wired = true
|
||||
@
|
||||
|
||||
# Disables the features that automate dropbox.js testing.
|
||||
unwire: ->
|
||||
return unless @wired
|
||||
chrome.contentSettings.popups.clear({})
|
||||
@wired = false
|
||||
@
|
||||
|
||||
# Global Automator instance.
|
||||
automator = new Automator()
|
||||
|
||||
# Current extension id, used to validate incoming messages.
|
||||
extensionId = chrome.i18n.getMessage "@@extension_id"
|
||||
|
||||
# Communicates with content scripts.
|
||||
chrome.extension.onMessage.addListener (message, sender, sendResponse) ->
|
||||
return unless sender.id is extensionId
|
||||
switch message.type
|
||||
when 'auth'
|
||||
sendResponse automate: automator.shouldAutomateAuth(message.url)
|
||||
when 'close'
|
||||
if automator.shouldAutomateClose(message.url) and sender.tab
|
||||
chrome.tabs.remove sender.tab.id
|
||||
|
||||
# Listen to pref changes and activate / deactivate the extension.
|
||||
chrome.storage.onChanged.addListener (changes, namespace) ->
|
||||
return unless namespace is 'sync'
|
||||
for name, change of changes
|
||||
continue unless name is 'enabled'
|
||||
automator.lifeSwitch change.newValue is 'true'
|
||||
|
||||
# The browser action item flips the switch that activates the extension.
|
||||
chrome.browserAction.onClicked.addListener ->
|
||||
chrome.storage.sync.get 'enabled', (values) ->
|
||||
enabled = values.enabled is 'true'
|
||||
chrome.storage.sync.set enabled: (!enabled).toString()
|
||||
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# Content script for Dropbox authorization pages.
|
||||
|
||||
message = type: 'auth', url: window.location.href
|
||||
chrome.extension.sendMessage message, (response) ->
|
||||
return unless response.automate
|
||||
|
||||
button = document.querySelector('[name=allow_access]') or
|
||||
document.querySelector '.freshbutton-blue'
|
||||
event = document.createEvent 'MouseEvents'
|
||||
|
||||
clientX = button.clientWidth / 2
|
||||
clientY = button.clientHeight / 2
|
||||
screenX = window.screenX + button.offsetLeft + clientX
|
||||
screenY = window.screenY + button.offsetTop + clientY
|
||||
event.initMouseEvent 'click', true, true, window, 1,
|
||||
screenX, screenY, clientX, clientY, false, false, false, false, 0, null
|
||||
button.dispatchEvent event
|
||||
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# Content script for Dropbox OAuth receiver pages.
|
||||
|
||||
message = type: 'close', url: window.location.href
|
||||
chrome.extension.sendMessage message
|
||||
|
Before Width: | Height: | Size: 611 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 745 B |