my_bitcasa 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +29 -0
  6. data/Rakefile +1 -0
  7. data/example/delete.rb +17 -0
  8. data/example/directory.rb +15 -0
  9. data/example/download_photo_legacy_thumbs.rb +27 -0
  10. data/example/download_photo_thumbs.rb +27 -0
  11. data/example/download_photos.rb +27 -0
  12. data/example/list.rb +15 -0
  13. data/example/login.rb +15 -0
  14. data/example/mkdir.rb +15 -0
  15. data/example/recursive_directory.rb +27 -0
  16. data/example/rename.rb +19 -0
  17. data/example/setting.yml.example +2 -0
  18. data/example/share.rb +19 -0
  19. data/example/upload.rb +18 -0
  20. data/example/zip_download_photos.rb +26 -0
  21. data/lib/my_bitcasa/api_error.rb +6 -0
  22. data/lib/my_bitcasa/authorization_error.rb +6 -0
  23. data/lib/my_bitcasa/bitcasa_base.rb +11 -0
  24. data/lib/my_bitcasa/bitcasa_drive.rb +15 -0
  25. data/lib/my_bitcasa/bitcasa_file.rb +61 -0
  26. data/lib/my_bitcasa/bitcasa_folder.rb +46 -0
  27. data/lib/my_bitcasa/bitcasa_item.rb +38 -0
  28. data/lib/my_bitcasa/bitcasa_share.rb +13 -0
  29. data/lib/my_bitcasa/connection.rb +102 -0
  30. data/lib/my_bitcasa/connection_error.rb +6 -0
  31. data/lib/my_bitcasa/connection_finalizer.rb +12 -0
  32. data/lib/my_bitcasa/connection_pool.rb +26 -0
  33. data/lib/my_bitcasa/data_accessor.rb +31 -0
  34. data/lib/my_bitcasa/delete.rb +26 -0
  35. data/lib/my_bitcasa/directory.rb +59 -0
  36. data/lib/my_bitcasa/download.rb +105 -0
  37. data/lib/my_bitcasa/downloadable.rb +60 -0
  38. data/lib/my_bitcasa/error.rb +4 -0
  39. data/lib/my_bitcasa/legacy_thumbnail.rb +33 -0
  40. data/lib/my_bitcasa/list.rb +71 -0
  41. data/lib/my_bitcasa/login_engine/phantomjs.rb +29 -0
  42. data/lib/my_bitcasa/login_engine/phantomjs_login.js +58 -0
  43. data/lib/my_bitcasa/login_engine/pure.rb +47 -0
  44. data/lib/my_bitcasa/login_engine/selenium.rb +87 -0
  45. data/lib/my_bitcasa/login_engine.rb +15 -0
  46. data/lib/my_bitcasa/mkdir.rb +26 -0
  47. data/lib/my_bitcasa/profile.rb +39 -0
  48. data/lib/my_bitcasa/rename.rb +24 -0
  49. data/lib/my_bitcasa/response_format_error.rb +6 -0
  50. data/lib/my_bitcasa/response_middleware.rb +39 -0
  51. data/lib/my_bitcasa/share.rb +30 -0
  52. data/lib/my_bitcasa/thumbnail.rb +33 -0
  53. data/lib/my_bitcasa/upload.rb +42 -0
  54. data/lib/my_bitcasa/version.rb +3 -0
  55. data/lib/my_bitcasa/zip_download.rb +21 -0
  56. data/lib/my_bitcasa.rb +38 -0
  57. data/my_bitcasa.gemspec +28 -0
  58. metadata +198 -0
@@ -0,0 +1,102 @@
1
+ require 'my_bitcasa/connection_finalizer'
2
+ require 'my_bitcasa/response_format_error'
3
+ require 'my_bitcasa/connection_error'
4
+ require 'my_bitcasa/authorization_error'
5
+ require 'faraday'
6
+ require 'faraday_middleware'
7
+ require 'my_bitcasa/response_middleware'
8
+ require 'active_support/core_ext'
9
+ require 'uri'
10
+
11
+ module MyBitcasa
12
+ class Connection < Faraday::Connection
13
+ attr_writer :login_engine
14
+ attr_accessor :cookie
15
+
16
+ def initialize(user: nil, password: nil, multipart: false)
17
+ super(:url => 'https://my.bitcasa.com') do |conn|
18
+ conn.use FaradayMiddleware::FollowRedirects
19
+ if multipart
20
+ conn.request :multipart
21
+ else
22
+ conn.request :url_encoded
23
+ end
24
+ #conn.response :logger
25
+ conn.response :my_bitcasa
26
+ conn.adapter Faraday.default_adapter
27
+ end
28
+ @headers[:user_agent] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:10.0.2) Gecko/20100101 Firefox/10.0.2"
29
+
30
+ yield self if block_given?
31
+
32
+ login(user, password) if user && password
33
+ ensure
34
+ #ObjectSpace.define_finalizer(self) { logout! }
35
+ end
36
+
37
+ def login_engine
38
+ @login_engine ||= LoginEngine.autodetect.new
39
+ end
40
+
41
+ def login(user, password)
42
+ login_engine.login(user, password)
43
+ @cookie = login_engine.cookie
44
+ end
45
+
46
+ def loggedin?
47
+ !!@cookie
48
+ end
49
+
50
+ def logout!
51
+ if loggedin?
52
+ self.get("/logout")
53
+ @cookie = nil
54
+ end
55
+ end
56
+
57
+ [:get, :post, :put, :delete, :head, :patch].each do |method|
58
+ class_eval %{
59
+ def #{method}_with_session(*args, &block)
60
+ res = #{method}_without_session(*args, &block)
61
+ _after_request(:#{method}, res)
62
+ res
63
+ end
64
+ alias_method_chain :#{method}, :session
65
+
66
+ def #{method}_with_loggedin(*args, &block)
67
+ _before_request(:#{method}, *args)
68
+ res = #{method}_without_loggedin(*args) {|req|
69
+ req.headers["Cookie"] = @cookie if @cookie
70
+ block.call(req) if block
71
+ }
72
+ res
73
+ end
74
+ alias_method_chain :#{method}, :loggedin
75
+ }
76
+ end
77
+
78
+ def multipart
79
+ @multipart ||= self.class.new(multipart: true)
80
+ @multipart.cookie = self.cookie
81
+ @multipart
82
+ end
83
+
84
+ private
85
+ def _before_request(method, *args)
86
+ raise AuthorizationError, "login required" unless loggedin?
87
+ end
88
+
89
+ def _after_request(method, res)
90
+ @cookie = res.headers["set-cookie"] || @cookie
91
+ if @cookie
92
+ @cookie.sub!(/; Domain=(\.?my)?.bitcasa.com; Path=\//, "")
93
+ end
94
+ end
95
+
96
+ class << self
97
+ def uri_encode(path)
98
+ URI.encode(path).gsub("[", "%5B").gsub("]", "%5D")
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,6 @@
1
+ require 'my_bitcasa/error'
2
+
3
+ module MyBitcasa
4
+ class ConnectionError < Error
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module MyBitcasa
2
+ class ConnectionFinalizer
3
+ def initialize(conn)
4
+ @conn = conn
5
+ end
6
+
7
+ def call(*args)
8
+ puts "finalizer logout"
9
+ @conn.logout!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ require 'my_bitcasa/connection'
2
+ require 'active_support/core_ext'
3
+
4
+ module MyBitcasa
5
+ module ConnectionPool
6
+ extend ActiveSupport::Concern
7
+
8
+ attr_writer :connection
9
+
10
+ def connection
11
+ @connection || self.class.connection
12
+ end
13
+
14
+ def multipart_connection
15
+ connection.multipart
16
+ end
17
+
18
+ module ClassMethods
19
+ mattr_accessor :connection
20
+
21
+ def establish_connection(*args)
22
+ @@connection = Connection.new(*args)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module MyBitcasa
2
+ module DataAccessor
3
+ def data_value_reader(key)
4
+ class_eval %{
5
+ def #{key}
6
+ @data["#{key}"]
7
+ end
8
+ }
9
+ end
10
+ private :data_value_reader
11
+
12
+ def data_bool_reader(key)
13
+ class_eval %{
14
+ def #{key}?
15
+ !!@data["#{key}"]
16
+ end
17
+ }
18
+ end
19
+ private :data_bool_reader
20
+
21
+ def data_reader(key)
22
+ key, question = key.to_s.split("?", -1)
23
+ if question
24
+ data_bool_reader(key)
25
+ else
26
+ data_value_reader(key)
27
+ end
28
+ end
29
+ private :data_reader
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ require 'my_bitcasa/connection_pool'
2
+ require 'json'
3
+
4
+ module MyBitcasa
5
+ class Delete
6
+ include ConnectionPool
7
+
8
+ def initialize(*paths)
9
+ @paths = paths.flatten
10
+ end
11
+
12
+ def delete
13
+ res = connection.post do |req|
14
+ req.url "/delete"
15
+ req.body = {
16
+ selection: JSON.generate({
17
+ paths: @paths,
18
+ albums: {},
19
+ artists: [],
20
+ photo_albums: [],
21
+ }),
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ require 'my_bitcasa/connection_pool'
2
+ require 'my_bitcasa/bitcasa_item'
3
+
4
+ module MyBitcasa
5
+ class Directory
6
+ include ConnectionPool
7
+ include Enumerable
8
+
9
+ attr_accessor :path
10
+ attr_accessor :top
11
+ attr_accessor :bottom
12
+ attr_accessor :sort_column
13
+ attr_accessor :sort_ascending
14
+ attr_accessor :show_incomplete
15
+ attr_accessor :seamless
16
+
17
+ def initialize(path, top: 0, bottom: 500, sort_column: :name, sort_ascending: true, show_incomplete: true, seamless: true)
18
+ @path = path.sub(/^\/?/, "/")
19
+ @top = top
20
+ @bottom = bottom
21
+ @sort_column = sort_column
22
+ @sort_ascending = sort_ascending
23
+ @show_incomplete = show_incomplete
24
+ @seamless = seamless
25
+ end
26
+
27
+ def each
28
+ top = @top
29
+
30
+ begin
31
+ res = connection.get {|req|
32
+ req.url Connection.uri_encode("/directory#{@path}")
33
+ req.params = {
34
+ top: top,
35
+ bottom: @bottom,
36
+ sort_column: @sort_column,
37
+ sort_ascending: @sort_ascending,
38
+ "show-incomplete" => @show_incomplete,
39
+ }
40
+ }
41
+
42
+ sentinel = res.body["sentinel"]
43
+ length = res.body["length"]
44
+ top = res.body["range"]["top"]
45
+ bottom = res.body["range"]["bottom"]
46
+ name = res.body["name"]
47
+ items = res.body["items"]
48
+
49
+ items.each do |item|
50
+ yield BitcasaItem.create(item)
51
+ end
52
+
53
+ if length<=bottom
54
+ break
55
+ end
56
+ end while @seamless
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,105 @@
1
+ require 'my_bitcasa/connection_pool'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'cgi'
5
+ require 'fileutils'
6
+ require 'tempfile'
7
+
8
+ module MyBitcasa
9
+ class Download
10
+ include ConnectionPool
11
+
12
+ def initialize(path, params, basename)
13
+ @path = path
14
+ @params = params
15
+ @basename = basename
16
+ @req_class = Net::HTTP::Get
17
+ end
18
+
19
+ def stream(&block)
20
+ # path
21
+ query = @params.map{|k,v|
22
+ "#{k}=#{CGI.escape(v.to_s)}"
23
+ }.join("&")
24
+
25
+ # headers
26
+ headers = connection.headers.dup
27
+ headers["Cookie"] = connection.cookie
28
+
29
+ # body
30
+ body = nil
31
+ if @body
32
+ body = @body.map{|k,v|
33
+ "#{k}=#{CGI.escape(v.to_s)}"
34
+ }.join("&")
35
+ end
36
+
37
+ # http
38
+ http = Net::HTTP.new(connection.url_prefix.host, connection.url_prefix.port)
39
+ http.use_ssl = connection.url_prefix.scheme=="https"
40
+
41
+ # request
42
+ req = @req_class.new(@path+"?"+query, headers)
43
+ http.request(req, body) do |res|
44
+ case res
45
+ when Net::HTTPSuccess
46
+ # 200 OK
47
+ when Net::HTTPUnauthorized
48
+ # 401 Auhorization error
49
+ raise AuthorizationError, "login required"
50
+ else
51
+ # other status
52
+ raise ConnectionError, "response status code: #{res.code}"
53
+ end
54
+ res.read_body(&block)
55
+ end
56
+ end
57
+
58
+ def save(dest_path, use_tempfile=true)
59
+ # normalize dest path
60
+ if File.directory?(dest_path)
61
+ dest_path = dest_path.sub(/\/?$/, "/") + @basename
62
+ end
63
+
64
+ # check dest dir
65
+ dest_dir = File.dirname(dest_path)
66
+ unless File.directory?(dest_dir)
67
+ raise Errno::ENOENT, "No such directory - #{dest_dir}"
68
+ end
69
+ unless File.writable?(dest_dir)
70
+ raise Errno::EACCES, "Permission denied - #{dest_dir}"
71
+ end
72
+
73
+ # download
74
+ if use_tempfile
75
+ # use tempfile
76
+ temp_path = nil
77
+ begin
78
+ Tempfile.open("bitcasa_tempfile_") {|f|
79
+ temp_path = f.path
80
+ self.stream {|x|
81
+ f.write(x)
82
+ }
83
+ }
84
+ FileUtils.mv(temp_path, dest_path)
85
+ ensure
86
+ if temp_path && File.file?(temp_path)
87
+ File.unlink(temp_path) rescue nil
88
+ end
89
+ end
90
+ else
91
+ # direct write
92
+ open(dest_path, "w") {|f|
93
+ self.stream {|x|
94
+ f.write(x)
95
+ }
96
+ }
97
+ end
98
+
99
+ # chmod
100
+ File.chmod(0644, dest_path)
101
+
102
+ dest_path
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,60 @@
1
+ require 'my_bitcasa/download'
2
+ require 'active_support/core_ext'
3
+ require 'cgi'
4
+ require 'fileutils'
5
+ require 'tempfile'
6
+
7
+ module MyBitcasa
8
+ module Downloadable
9
+ extend ActiveSupport::Concern
10
+
11
+ def stream(&block)
12
+ # path
13
+ download = Download.new(_download_path, _download_params)
14
+ download.stream(&block)
15
+ end
16
+
17
+ def save(dest_path, use_tempfile=true)
18
+ download = Download.new(_download_path, _download_params, _download_basename)
19
+ download.save(dest_path, use_tempfile)
20
+ end
21
+
22
+ # downloadable info
23
+
24
+ def _download_path
25
+ path_proc = self.class.downloadable_path_proc
26
+ instance_eval &path_proc
27
+ end
28
+ private :_download_path
29
+
30
+ def _download_params
31
+ params_proc = self.class.downloadable_params_proc
32
+ instance_eval &params_proc
33
+ end
34
+ private :_download_params
35
+
36
+ def _download_basename
37
+ basename_proc = self.class.downloadable_basename_proc
38
+ instance_eval &basename_proc
39
+ end
40
+ private :_download_basename
41
+
42
+ module ClassMethods
43
+ attr_reader :downloadable_path_proc
44
+ attr_reader :downloadable_params_proc
45
+ attr_reader :downloadable_basename_proc
46
+
47
+ def downloadable_path(&block)
48
+ @downloadable_path_proc = block
49
+ end
50
+
51
+ def downloadable_params(&block)
52
+ @downloadable_params_proc = block
53
+ end
54
+
55
+ def downloadable_basename(&block)
56
+ @downloadable_basename_proc = block
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+ module MyBitcasa
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,33 @@
1
+ require 'my_bitcasa/connection_pool'
2
+ require 'my_bitcasa/downloadable'
3
+
4
+ module MyBitcasa
5
+ class LegacyThumbnail
6
+ include ConnectionPool
7
+ include Downloadable
8
+
9
+ THUMB_SIZE = {
10
+ small: "35x35c",
11
+ medium: "150x150c",
12
+ large: "260x260c",
13
+ preview: "1024x768s",
14
+ }.freeze
15
+
16
+ downloadable_path {
17
+ "/file/#{@file.id}/thumbnail/#{@specific_size}.png"
18
+ }
19
+ downloadable_params {{
20
+ size: @file.size,
21
+ mime: @file.mime,
22
+ }}
23
+ downloadable_basename {
24
+ name = File.basename(@file.name, "."+@file.extension)
25
+ "#{name}_#{@specific_size}.png"
26
+ }
27
+
28
+ def initialize(file, size=:small)
29
+ @file = file
30
+ @specific_size = THUMB_SIZE[size] || size
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,71 @@
1
+ require 'my_bitcasa/connection_pool'
2
+ require 'my_bitcasa/bitcasa_item'
3
+
4
+ module MyBitcasa
5
+ class List
6
+ include ConnectionPool
7
+ include Enumerable
8
+
9
+ EVERYTHING = "everything".freeze
10
+ PHOTOS = "photos".freeze
11
+ MUSIC = "music".freeze
12
+ MUSIC_ALBUMS = "music/albums".freeze
13
+ MUSIC_ARTISTS = "music/artists".freeze
14
+ VIDEOS = "videos".freeze
15
+ DOCUMENTS = "documents".freeze
16
+
17
+ TYPES = Set[EVERYTHING, PHOTOS, MUSIC, MUSIC_ALBUMS, MUSIC_ARTISTS, VIDEOS, DOCUMENTS].freeze
18
+
19
+ attr_accessor :type
20
+ attr_accessor :search
21
+ attr_accessor :top
22
+ attr_accessor :bottom
23
+ attr_accessor :sort_column
24
+ attr_accessor :sort_ascending
25
+ attr_accessor :seamless
26
+
27
+ def initialize(type: EVERYTHING, search: nil, top: 0, bottom: 500, sort_column: :name, sort_ascending: true, seamless: true)
28
+ raise "type error: #{type}" unless TYPES.include?(type)
29
+
30
+ @type = type
31
+ @search = search
32
+ @top = top
33
+ @bottom = bottom
34
+ @sort_column = sort_column
35
+ @sort_ascending = sort_ascending
36
+ @seamless = seamless
37
+ end
38
+
39
+ def each
40
+ top = @top
41
+
42
+ begin
43
+ res = connection.get {|req|
44
+ req.url "/list/#{@type}"
45
+ req.params = {
46
+ search: @search,
47
+ top: top,
48
+ bottom: @bottom,
49
+ sort_column: @sort_column,
50
+ sort_ascending: @sort_ascending,
51
+ }
52
+ }
53
+
54
+ sentinel = res.body["sentinel"]
55
+ length = res.body["length"]
56
+ top = res.body["range"]["top"]
57
+ bottom = res.body["range"]["bottom"]
58
+ name = res.body["name"]
59
+ items = res.body["items"]
60
+
61
+ items.each do |item|
62
+ yield BitcasaItem.create(item)
63
+ end
64
+
65
+ if length<=bottom
66
+ break
67
+ end
68
+ end while @seamless
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ require 'my_bitcasa/authorization_error'
2
+
3
+ module MyBitcasa
4
+ module LoginEngine
5
+ class Phantomjs
6
+ attr_reader :cookie
7
+
8
+ def initialize
9
+ require 'phantomjs'
10
+ end
11
+
12
+ def login(user, password)
13
+ js_path = ::File.expand_path("phantomjs_login.js", ::File.dirname(__FILE__))
14
+ cookie = ::Phantomjs.run(js_path, user, password)
15
+ cookie = cookie.to_s.strip
16
+ if cookie.length==0
17
+ raise AuthorizationError, "login failure"
18
+ end
19
+ @cookie = cookie
20
+ end
21
+
22
+ class << self
23
+ def available?
24
+ new && true rescue false
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,58 @@
1
+ var page = require('webpage').create();
2
+ var args = require('system').args;
3
+
4
+ var user = args[1]==void 0 ? "" : args[1];
5
+ var password = args[2]==void 0 ? "" : args[2];
6
+
7
+ page.onInitialized = function() {
8
+ page.evaluate(function() {
9
+ document.addEventListener('DOMContentLoaded', function() {
10
+ window.callPhantom('DOMContentLoaded');
11
+ }, false);
12
+ });
13
+ };
14
+
15
+ var funcs = function(funcs) {
16
+ this.funcs = funcs;
17
+ this.init();
18
+ };
19
+ funcs.prototype = {
20
+ init: function() {
21
+ var self = this;
22
+ page.onCallback = function(data){
23
+ if (data === 'DOMContentLoaded') self.next();
24
+ }
25
+ },
26
+ next: function() {
27
+ var func = this.funcs.shift();
28
+ if (func !== undefined) {
29
+ func();
30
+ } else {
31
+ page.onCallback = function(){};
32
+ }
33
+ }
34
+ };
35
+
36
+ new funcs([
37
+ function() {
38
+ page.open('https://my.bitcasa.com/login');
39
+ },
40
+ function() {
41
+ page.evaluate(function(user, password) {
42
+ document.getElementById('user').value = user;
43
+ document.getElementById('password').value = password;
44
+ document.querySelector('form').submit();
45
+ }, user, password);
46
+ },
47
+ function() {
48
+ var cookie = page.evaluate(function() {
49
+ if (window.location.pathname.indexOf("/login")==0) {
50
+ return null;
51
+ } else {
52
+ return document.cookie;
53
+ }
54
+ });
55
+ console.log(cookie);
56
+ phantom.exit();
57
+ }
58
+ ]).next();
@@ -0,0 +1,47 @@
1
+ require 'my_bitcasa/response_format_error'
2
+
3
+ module MyBitcasa
4
+ module LoginEngine
5
+ # MyBitcasa::LoginEngine::Pure is not work.
6
+ class Pure
7
+ include ConnectionPool
8
+
9
+ attr_reader :cookie
10
+
11
+ def initialize(connection=nil)
12
+ @connection = connection
13
+ end
14
+
15
+ def login(user, password)
16
+ # login form
17
+ res = @conn.get_without_loggedin("/login")
18
+
19
+ csrf_tag = res.body.match(/<input [^<>]*name="csrf_token"[^<>]*>/){|m| m[0]}
20
+ raise ResponseFormatError, "csrf_token tag is not found" unless csrf_tag
21
+
22
+ csrf_token = csrf_tag.match(/value="([^"]+)"/){|m| m[1]}
23
+ raise ResponseFormatError, "csrf_token is not found" unless csrf_token
24
+
25
+ # login post
26
+ res = @conn.post_without_loggedin("/login", {
27
+ user: user,
28
+ password: password,
29
+ csrf_token: csrf_token,
30
+ redirect: "/",
31
+ })
32
+
33
+ if res.env[:url].path.start_with?("/login")
34
+ raise AuthorizationError, "login failure"
35
+ end
36
+
37
+ @cookie = res.headers["set-cookie"]
38
+ end
39
+
40
+ class << self
41
+ def available?
42
+ false
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end