Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[feature] Dropbox: API client

  • Loading branch information...
commit 66f15deb9cab25c4e23adce52e2dcd1c58921794 1 parent 18aa3f1
authored January 20, 2012

Showing 1 changed file with 469 additions and 0 deletions. Show diff stats Hide diff stats

  1. 469  stdlib/apis/dropbox/dropbox.opa
469  stdlib/apis/dropbox/dropbox.opa
... ...
@@ -0,0 +1,469 @@
  1
+/*
  2
+    Copyright © 2012 MLstate
  3
+
  4
+    This file is part of OPA.
  5
+
  6
+    OPA is free software: you can redistribute it and/or modify it under the
  7
+    terms of the GNU Affero General Public License, version 3, as published by
  8
+    the Free Software Foundation.
  9
+
  10
+    OPA is distributed in the hope that it will be useful, but WITHOUT ANY
  11
+    WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12
+    FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for
  13
+    more details.
  14
+
  15
+    You should have received a copy of the GNU Affero General Public License
  16
+    along with OPA.  If not, see <http://www.gnu.org/licenses/>.
  17
+*/
  18
+/*
  19
+ * Author    : Nicolas Glondu <nicolas.glondu@mlstate.com>
  20
+ **/
  21
+
  22
+/**
  23
+ * Dropbox generic API module (v1)
  24
+ *
  25
+ * @category api
  26
+ * @author Nicolas Glondu, 2011
  27
+ * @destination public
  28
+ */
  29
+
  30
+import stdlib.apis.common
  31
+import stdlib.apis.oauth
  32
+
  33
+/**
  34
+ * Dropbox configuration
  35
+ *
  36
+ * To obtain a credentials, visit:
  37
+ *  https://www.dropbox.com/developers/apps
  38
+ */
  39
+type Dropbox.conf = {
  40
+  app_key    : string
  41
+  app_secret : string
  42
+}
  43
+
  44
+type Dropbox.creds = {
  45
+  token  : string
  46
+  secret : string
  47
+}
  48
+
  49
+type Dropbox.metadata_options = {
  50
+  file_limit      : int
  51
+  hash            : option(string)
  52
+  list            : bool
  53
+  include_deleted : bool
  54
+  rev             : option(int)
  55
+}
  56
+
  57
+type Dropbox.thumb_format = {jpeg} / {png}
  58
+type Dropbox.thumb_size =
  59
+    {small}  // 32x32
  60
+  / {medium} // 64x64
  61
+  / {large}  // 128x128
  62
+  / {s}      // 64x64
  63
+  / {m}      // 128x128
  64
+  / {l}      // 640x480
  65
+  / {xl}     // 1024x768
  66
+
  67
+/* Types returned by API */
  68
+
  69
+type Dropbox.metadata = {
  70
+  rev          : string
  71
+  thumb_exists : bool
  72
+  size         : int // bytes
  73
+  size_text    : string
  74
+  modified     : option(Date.date)
  75
+  path         : string
  76
+  icon         : string
  77
+  root         : string
  78
+  is_deleted   : bool
  79
+}
  80
+
  81
+/**
  82
+ * Type of an element in a Dropbox folder
  83
+ * 
  84
+ * Note that an empty folder will have its [content] field
  85
+ * to [some([])] and that [none] for this field just means
  86
+ * that there was no information about the folder files.
  87
+ */
  88
+type Dropbox.element =
  89
+    { file
  90
+      metadata  : Dropbox.metadata
  91
+      mime_type : string }
  92
+  / { folder
  93
+      metadata  : Dropbox.metadata
  94
+      contents  : option(list(Dropbox.element)) }
  95
+
  96
+type Dropbox.quota_info = {
  97
+  shared : int
  98
+  normal : int
  99
+  total  : int
  100
+}
  101
+
  102
+type Dropbox.info = {
  103
+  email         : string
  104
+  display_name  : string
  105
+  referral_link : string
  106
+  uid           : int
  107
+  country       : string
  108
+  quota_info    : Dropbox.quota_info
  109
+}
  110
+
  111
+type Dropbox.url = {
  112
+  url : string
  113
+  expires : Date.date
  114
+}
  115
+
  116
+type Dropbox.file = {
  117
+  content : binary
  118
+  mime_type : string
  119
+}
  120
+
  121
+@private DBParse = {{
  122
+
  123
+  /**
  124
+   * Example of date: Fri, 20 Jan 2012 16:18:23 +0000
  125
+   */
  126
+  parse_date(str) =
  127
+    int_of_text(t) = Int.of_string(Text.to_string(t))
  128
+    n = parser k=[0-9] -> k
  129
+    nn = parser v=(n+) -> int_of_text(v)
  130
+    do_shift(forward,h,min) =
  131
+      d = { Duration.zero with ~forward ~h ~min }
  132
+        |> Duration.of_human_readable
  133
+      Date.advance(_, d)      
  134
+    shift(forward,h,m) =
  135
+      do_shift(forward,int_of_text(h),int_of_text(m))
  136
+    tmz = parser
  137
+      | "Z" -> identity
  138
+      | "-" h=(n n) m=(n n) -> shift(true, h, m)
  139
+      | "-" h=(n n) ":" m=(n n) -> shift(true, h, m)
  140
+      | "+" h=(n n) m=(n n) -> shift(false, h, m)
  141
+      | "+" h=(n n) ":" m=(n n) -> shift(false, h, m)
  142
+      | .* -> identity
  143
+    mon = parser
  144
+      | "Jan" -> {january}
  145
+      | "Feb" -> {february}
  146
+      | "Mar" -> {march}
  147
+      | "Apr" -> {april}
  148
+      | "May" -> {may}
  149
+      | "Jun" -> {june}
  150
+      | "Jul" -> {july}
  151
+      | "Aug" -> {august}
  152
+      | "Sep" -> {september}
  153
+      | "Oct" -> {october}
  154
+      | "Nov" -> {november}
  155
+      | "Dec" -> {december}
  156
+    p = parser (!n .)* d=nn " " m=mon " " y=nn " " h=nn ":" min=nn ":" s=nn " " tmz=tmz ->
  157
+      tmz(Date.build({year=y month=m day=d h=h min=min s=s}))
  158
+    match Parser.try_parse(p, str) with
  159
+    | {some=d} -> d
  160
+    | {none} ->
  161
+      do Log.error("parse_date", "Can't parse '{str}'")
  162
+      Date.now()
  163
+
  164
+  build_quota(data) =
  165
+    map = JsonOpa.record_fields(data) ? Map.empty
  166
+    int(name) = API_libs_private.map_get_int(name, map)
  167
+    { shared = int("shared")
  168
+      normal = int("normal")
  169
+      total  = int("quota")
  170
+    } : Dropbox.quota_info
  171
+
  172
+  build_infos(data) =
  173
+    map = API_libs_private.parse_json(data.content)
  174
+      |> JsonOpa.record_fields
  175
+      |> Option.default(Map.empty, _)
  176
+    int(name) = API_libs_private.map_get_int(name, map)
  177
+    str(name) = API_libs_private.map_get_string(name, map)
  178
+    quota_info =
  179
+      StringMap.get("quota_info", map) ? {Record=[]}:RPC.Json.json
  180
+      |> build_quota
  181
+    { ~quota_info
  182
+      email         = str("email")
  183
+      referral_link = str("referral_link")
  184
+      display_name  = str("display_name")
  185
+      uid           = int("uid")
  186
+      country       = str("country")
  187
+    } : Dropbox.info
  188
+
  189
+  build_metadata_internal(elt) : Dropbox.element =
  190
+    map = JsonOpa.record_fields(elt) ? Map.empty
  191
+    int(name) = API_libs_private.map_get_int(name, map)
  192
+    str(name) = API_libs_private.map_get_string(name, map)
  193
+    bool(name) = API_libs_private.map_get_bool(name, map, false)
  194
+    modified =
  195
+      date_str = str("modified")
  196
+      if date_str == "" then none
  197
+      else some(parse_date(date_str))
  198
+    metadata = {
  199
+      rev          = str("rev")
  200
+      thumb_exists = bool("thumb_exists")
  201
+      size         = int("bytes")
  202
+      size_text    = str("size")
  203
+      modified     = modified
  204
+      path         = str("path")
  205
+      icon         = str("icon")
  206
+      root         = str("root")
  207
+      is_deleted   = bool("is_deleted")
  208
+    } : Dropbox.metadata
  209
+    is_dir = bool("is_dir")
  210
+    if is_dir then
  211
+      contents : option(list(Dropbox.element)) =
  212
+        match StringMap.get("contents", map) with
  213
+        | {some={List=l}} ->
  214
+          some(List.map(build_metadata_internal, l))
  215
+        | _ -> none
  216
+      {folder ~metadata ~contents}
  217
+    else
  218
+      mime_type = str("mime_type")
  219
+      {file ~metadata ~mime_type}
  220
+
  221
+  one_metadata(data) =
  222
+    parsed = API_libs_private.parse_json(data.content)
  223
+    build_metadata_internal(parsed)
  224
+
  225
+  metadata_list(data) =
  226
+    match API_libs_private.parse_json(data.content) with
  227
+    | {List=l} -> List.map(build_metadata_internal, l)
  228
+    | _ -> []
  229
+
  230
+  build_url(data) =
  231
+    map = API_libs_private.parse_json(data.content)
  232
+      |> JsonOpa.record_fields
  233
+      |> Option.default(Map.empty, _)
  234
+    str(name) = API_libs_private.map_get_string(name, map)
  235
+    { url     = str("url")
  236
+      expires = str("expires") |> parse_date
  237
+    } : Dropbox.url
  238
+
  239
+  build_file(data) =
  240
+    { content = data.content
  241
+      mime_type = data.mime_type
  242
+    } : Dropbox.file
  243
+
  244
+}}
  245
+
  246
+@private DBprivate(conf:Dropbox.conf) = {{
  247
+
  248
+  DBOAuth(http_method) = OAuth({
  249
+    consumer_key      = conf.app_key
  250
+    consumer_secret   = conf.app_secret
  251
+    auth_method       = {HMAC_SHA1}
  252
+    request_token_uri = "https://api.dropbox.com/1/oauth/request_token"
  253
+    authorize_uri     = "https://www.dropbox.com/1/oauth/authorize"
  254
+    access_token_uri  = "https://api.dropbox.com/1/oauth/access_token"
  255
+    http_method       = http_method
  256
+    inlined_auth      = false
  257
+    custom_headers    = none
  258
+  } : OAuth.parameters)
  259
+
  260
+  wget(host, path, params, creds:Dropbox.creds, parse_fun) =
  261
+    uri = "{host}{path}"
  262
+    res = DBOAuth({GET}).get_protected_resource_2(uri, params, creds.token, creds.secret)
  263
+    match res with
  264
+    | {success=s} -> {success=parse_fun(s)}
  265
+    | {failure=f} -> {failure=f}
  266
+
  267
+  wpost(host, path, params, creds:Dropbox.creds, parse_fun) =
  268
+    uri = "{host}{path}"
  269
+    res = DBOAuth({POST}).get_protected_resource_2(uri, params, creds.token, creds.secret)
  270
+    match res with
  271
+    | {success=s} -> {success=parse_fun(s)}
  272
+    | {failure=f} -> {failure=f}
  273
+
  274
+  wput(host, path, mimetype:string, file:binary, params, creds:Dropbox.creds, parse_fun) =
  275
+    uri = "{host}{path}"
  276
+    res = DBOAuth({PUT=~{mimetype file}}).get_protected_resource_2(uri, params, creds.token, creds.secret)
  277
+    match res with
  278
+    | {success=s} -> {success=parse_fun(s)}
  279
+    | {failure=f} -> {failure=f}
  280
+
  281
+}}
  282
+
  283
+Dropbox(conf:Dropbox.conf) = {{
  284
+
  285
+  // Note: V1 of the API
  286
+  @private api_host = "https://api.dropbox.com/1/"
  287
+  @private content_host = "https://api-content.dropbox.com/1/"
  288
+  @private DBP = DBprivate(conf)
  289
+
  290
+  OAuth = {{
  291
+
  292
+    get_request_token =
  293
+      DBP.DBOAuth({GET}).get_request_token
  294
+
  295
+    build_authorize_url(token, callback_url) =
  296
+      "{DBP.DBOAuth({GET}).build_authorize_url(token)}&oauth_callback={Uri.encode_string(callback_url)}"
  297
+
  298
+    connection_result =
  299
+      DBP.DBOAuth({GET}).connection_result
  300
+
  301
+    get_access_token =
  302
+      DBP.DBOAuth({GET}).get_access_token
  303
+
  304
+  }}
  305
+
  306
+  Account = {{
  307
+
  308
+    info(creds) =
  309
+      DBP.wget(api_host, "account/info", [], creds, DBParse.build_infos)
  310
+
  311
+  }}
  312
+
  313
+  default_metadata_options = {
  314
+    file_limit      = 10000
  315
+    hash            = none
  316
+    list            = true
  317
+    include_deleted = false
  318
+    rev             = none
  319
+  } : Dropbox.metadata_options
  320
+
  321
+  Files(root:string, file:string) = {{
  322
+
  323
+    @private file_path =
  324
+      file =
  325
+        if file == "" then "/"
  326
+        else if String.sub(0, 1, file) == "/" then file
  327
+        else "/{file}"
  328
+      "{root}{file}"
  329
+
  330
+    get(rev:option(int), creds) =
  331
+      path = "files/{file_path}"
  332
+      params = match rev with
  333
+        | {none} -> []
  334
+        | {some=r} -> [("rev", Int.to_string(r))]
  335
+      DBP.wget(content_host, path, params, creds, DBParse.build_file)
  336
+
  337
+    put(mimetype, file:binary, overwrite, parent_rev:option(int), creds) =
  338
+      path = "files_put/{file_path}"
  339
+      params = [
  340
+        ("overwrite", Bool.to_string(overwrite)),
  341
+      ] |> (
  342
+        match parent_rev with
  343
+        | {none} -> identity
  344
+        | {some=r} -> List.cons(("parent_rev", Int.to_string(r)), _)
  345
+      )
  346
+      do ignore(file)
  347
+      DBP.wput(content_host, path, mimetype, file, params, creds, DBParse.one_metadata)
  348
+
  349
+    @private format_metadata_options(o:Dropbox.metadata_options) =
  350
+      [ ("file_limit", Int.to_string(o.file_limit)),
  351
+        ("list", Bool.to_string(o.list)),
  352
+        ("include_deleted", Bool.to_string(o.include_deleted)),
  353
+      ] |> (
  354
+        match o.hash with
  355
+          | {none} -> identity
  356
+          | {some=h} -> List.cons(("hash", h), _)
  357
+      ) |> (
  358
+        match o.rev with
  359
+          | {none} -> identity
  360
+          | {some=r} -> List.cons(("rev", Int.to_string(r)), _)
  361
+      )
  362
+
  363
+    metadata(options, creds) =
  364
+      path = "metadata/{file_path}"
  365
+      params = format_metadata_options(options)
  366
+      DBP.wget(api_host, path, params, creds, DBParse.one_metadata)
  367
+
  368
+    /**
  369
+     * default: 10 - max: 1000
  370
+     */
  371
+    revisions(rev_limit:option(int), creds) =
  372
+      path = "revisions/{file_path}"
  373
+      params = match rev_limit with
  374
+        | {none} -> []
  375
+        | {some=l} -> [("rev_limit", Int.to_string(l))]
  376
+      DBP.wget(api_host, path, params, creds, DBParse.metadata_list)
  377
+
  378
+    restore(rev, creds) =
  379
+      path = "restore/{file_path}"
  380
+      params = [("rev", Int.to_string(rev))]
  381
+      DBP.wpost(api_host, path, params, creds, DBParse.one_metadata)
  382
+
  383
+    /**
  384
+     * default and max: 1000
  385
+     */
  386
+    search(query, include_deleted:bool, file_limit:option(int), creds) =
  387
+      path = "search/{file_path}"
  388
+      params = [
  389
+        ("query", query),
  390
+        ("include_deleted", Bool.to_string(include_deleted)),
  391
+      ] |> (
  392
+        match file_limit with
  393
+        | {none} -> identity
  394
+        | {some=l} -> List.cons(("file_limit", Int.to_string(l)), _)
  395
+      )
  396
+      DBP.wget(api_host, path, params, creds, DBParse.metadata_list)
  397
+
  398
+    shares(creds) =
  399
+      path = "shares/{file_path}"
  400
+      DBP.wpost(api_host, path, [], creds, DBParse.build_url)
  401
+
  402
+    media(creds) =
  403
+      path = "media/{file_path}"
  404
+      DBP.wpost(api_host, path, [], creds, DBParse.build_url)
  405
+
  406
+    /**
  407
+     * Prefer [jpeg] for photos while [png] is better for
  408
+     * screenshots and digital art
  409
+     */
  410
+    thumbnails(format:Dropbox.thumb_format, size:Dropbox.thumb_size, creds) =
  411
+      path = "thumbnails/{file_path}"
  412
+      format = match format with
  413
+        | {jpeg} -> "JPEG"
  414
+        | {png} -> "PNG"
  415
+      size = match size with
  416
+        | {small}  -> "small"
  417
+        | {medium} -> "medium"
  418
+        | {large}  -> "large"
  419
+        | {s}      -> "s"
  420
+        | {m}      -> "m"
  421
+        | {l}      -> "l"
  422
+        | {xl}     -> "xl"
  423
+      params = [
  424
+        ("format", format),
  425
+        ("size", size),
  426
+      ]
  427
+      DBP.wget(content_host, path, params, creds, DBParse.build_file)
  428
+
  429
+  }}
  430
+
  431
+  FileOps = {{
  432
+
  433
+    copy(root, from_path, to_path, creds) =
  434
+      path = "fileops/copy"
  435
+      params = [
  436
+        ("root", root),
  437
+        ("from_path", from_path),
  438
+        ("to_path", to_path),
  439
+      ]
  440
+      DBP.wpost(api_host, path, params, creds, DBParse.one_metadata)
  441
+
  442
+    create_folder(root, path, creds) =
  443
+      rpath = "fileops/create_folder"
  444
+      params = [
  445
+        ("root", root),
  446
+        ("path", path),
  447
+      ]
  448
+      DBP.wpost(api_host, rpath, params, creds, DBParse.one_metadata)
  449
+
  450
+    delete(root, path, creds) =
  451
+      rpath = "fileops/delete"
  452
+      params = [
  453
+        ("root", root),
  454
+        ("path", path),
  455
+      ]
  456
+      DBP.wpost(api_host, rpath, params, creds, DBParse.one_metadata)
  457
+
  458
+    move(root, from_path, to_path, creds) =
  459
+      path = "fileops/move"
  460
+      params = [
  461
+        ("root", root),
  462
+        ("from_path", from_path),
  463
+        ("to_path", to_path),
  464
+      ]
  465
+      DBP.wpost(api_host, path, params, creds, DBParse.one_metadata)
  466
+
  467
+  }}
  468
+
  469
+}}

0 notes on commit 66f15de

Please sign in to comment.
Something went wrong with that request. Please try again.