pcloud_api 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +137 -0
- data/RELEASEING.md +7 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/generate_access_token +46 -0
- data/bin/setup +8 -0
- data/lib/pcloud/client.rb +68 -0
- data/lib/pcloud/file/parser.rb +44 -0
- data/lib/pcloud/file.rb +137 -0
- data/lib/pcloud/folder/parser.rb +51 -0
- data/lib/pcloud/folder.rb +99 -0
- data/lib/pcloud/version.rb +3 -0
- data/lib/pcloud_api.rb +9 -0
- data/pcloud_api.gemspec +33 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e5ea6aab35912a377555ea6ee45e36ec29dbc797e620b4d738ea2665f2679e79
|
4
|
+
data.tar.gz: 5bf4514161d144db50fbbd919e8886e1adc3144e9df7381db23a7594170c1cca
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 23121677b95b60944d49d59623466841d08a7c1e1833013f6eaf6ca8e011799a26bc6bfadeb28b6c7c65e178cb51f5632e78ed76a2cd547a6d2d875dbbb4d1b8
|
7
|
+
data.tar.gz: f1900eaf066f7dba20e6b8ef9b0bfa3554e070d5f0ca2b3bbd7cb5d858cbbcb405e60c144ace91048c3635658752fd4ab232af068f058e835b7ac0e2fd23db83
|
data/.gitignore
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/_yardoc/
|
4
|
+
/coverage/
|
5
|
+
/doc/
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/tmp/
|
9
|
+
|
10
|
+
# rspec failure tracking
|
11
|
+
.rspec_status
|
12
|
+
|
13
|
+
*.DS_Store
|
14
|
+
Gemfile.lock
|
15
|
+
|
16
|
+
# Don't accidentally check in builds or unpacked builds of this gem
|
17
|
+
pcloud_api-*.gem
|
18
|
+
pcloud_api-*/
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2021 Joshua Hunsche Jones
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
# pCloud API
|
2
|
+
|
3
|
+
The `pcloud_api` gem provides an intuitive Ruby interface for interacting with the [pCloud API](https://docs.pcloud.com/) using OAuth2. This gem does not attempt to replicate the entire functionality of the pCloud API but rather to provide quick and easy access for its most basic and common functionality. If you are looking for a lower-level pCloud API wrapper, [`pcloud`](https://github.com/7urkm3n/pcloud) might be a better fit for you.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'pcloud_api'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install pcloud_api
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Configuration
|
24
|
+
|
25
|
+
The `Pcloud::Client` can be configured by directly calling the `Pcloud::Client.configure` method in an initializer or somewhere else in your code at startup:
|
26
|
+
```ruby
|
27
|
+
Pcloud::Client.configure(
|
28
|
+
access_token: "your-pcloud-app-access-token",
|
29
|
+
data_region: "EU"
|
30
|
+
)
|
31
|
+
```
|
32
|
+
|
33
|
+
You may also choose to configure the client using the `PCLOUD_API_DATA_REGION` and `PCLOUD_API_ACCESS_TOKEN` environment variables if you prefer. Advanced users can also set additional configuration values including `PCLOUD_API_TIMEOUT_SECONDS` and `PCLOUD_API_BASE_URI`.
|
34
|
+
|
35
|
+
### Available operations
|
36
|
+
|
37
|
+
There are two main objects represented in the library, `Pcloud::File` and `Pcloud::Folder`. Both attempt to present an intuitive interface that will feel quickly familiar to Ruby and Ruby on Rails developers.
|
38
|
+
|
39
|
+
The `Pcloud::File` API includes:
|
40
|
+
```ruby
|
41
|
+
# Find files by file id or path:
|
42
|
+
Pcloud::File.find(1)
|
43
|
+
Pcloud::File.find_by(path: "/images/jack_the_cat.jpg")
|
44
|
+
|
45
|
+
# Upload a new file, rename if already existing:
|
46
|
+
Pcloud::File.upload(
|
47
|
+
folder_id: 1,
|
48
|
+
filename: "jack_goes_swimming.mp4",
|
49
|
+
file: File.open("/Users/joshua/Downloads/jack_goes_swimming.mp4")
|
50
|
+
)
|
51
|
+
# NOTE: the upload method will allow you to specify either the `path` or the
|
52
|
+
# `folder_id`, or you can choose to pass neither paramenter and your files will
|
53
|
+
# be placed in your root pCloud directory.
|
54
|
+
|
55
|
+
# Upload a file, force overwrite of existing file:
|
56
|
+
Pcloud::File.upload!(
|
57
|
+
folder_id: 1,
|
58
|
+
filename: "jack_goes_swimming.mp4",
|
59
|
+
file: File.open("/Users/joshua/Downloads/jack_goes_swimming.mp4")
|
60
|
+
)
|
61
|
+
|
62
|
+
# Rename a file:
|
63
|
+
jack_goes_swimming.update(name: "That's one wet cat.mp4")
|
64
|
+
|
65
|
+
# Move a file by updating the parent_folder_id or path:
|
66
|
+
jack_goes_swimming.update(parent_folder_id: 9000)
|
67
|
+
|
68
|
+
# Delete a file:
|
69
|
+
jack_goes_swimming.delete
|
70
|
+
|
71
|
+
# Get a link to download a file:
|
72
|
+
jack_goes_swimming.download_link
|
73
|
+
|
74
|
+
# Access the parent folder of a file:
|
75
|
+
jack_goes_swimming.parent_folder
|
76
|
+
```
|
77
|
+
|
78
|
+
The `Pcloud::Folder` API is very similar:
|
79
|
+
```ruby
|
80
|
+
# Find folders by folder id or path:
|
81
|
+
Pcloud::Folder.find(1)
|
82
|
+
Pcloud::Folder.find_by(path: "/images")
|
83
|
+
|
84
|
+
# Create a new folder by parent_folder_id and name:
|
85
|
+
Pcloud::Folder.first_or_create(parent_folder_id: 9000, name: "jack")
|
86
|
+
|
87
|
+
# Create a new folder by path:
|
88
|
+
Pcloud::Folder.first_or_create(path: "/images/jack")
|
89
|
+
|
90
|
+
# Rename a folder:
|
91
|
+
jack_images.update(name: "Jack's Photo Library")
|
92
|
+
|
93
|
+
# Move a folder by updating the parent_folder_id or path:
|
94
|
+
jack_images.update(parent_folder_id: 9000)
|
95
|
+
|
96
|
+
# Delete an empty folder:
|
97
|
+
jack_images.delete
|
98
|
+
|
99
|
+
# Recursively delete a folder and all its contents:
|
100
|
+
jack_images.delete!
|
101
|
+
|
102
|
+
# Access the parent folder of a folder:
|
103
|
+
jack_images.parent_folder
|
104
|
+
|
105
|
+
# Access the contents of a folder:
|
106
|
+
jack_images.contents
|
107
|
+
```
|
108
|
+
|
109
|
+
**Aside: path vs id:**
|
110
|
+
pCloud recommends using the `folder_id`, `parent_folder_id` or `file_id` params for API calls whenever possible, rather using an exact path. Folder and file ids are static, so this will make your code less brittle to changes in the file/folder tree. You can simply look up the id for a folder in the console ahead of time and then set it in your code, similar to how you would specify an AWS S3 bucket.
|
111
|
+
|
112
|
+
|
113
|
+
**Aside: off-label client use:**
|
114
|
+
The `Pcloud::File` and `Pcloud::Folder` APIs cover the most important, common functionality developers will want to access in a way that is easy to use without much onboarding. If you find that you still need to access other parts of the pCloud API that are not included in this gem yet, you can try calling other methods specified in [the pCloud API docs](https://docs.pcloud.com/) by interacting directly with the `Pcloud::Client`:
|
115
|
+
```ruby
|
116
|
+
Pcloud::Client.execute("listrevisions", query: { fileid: 90000 })
|
117
|
+
```
|
118
|
+
_(There are a few methods on the raw pCloud API that require manual login, which this gem does not yet support. If you find that you need access to these methods you may wish to look at using the [`pcloud`](https://github.com/7urkm3n/pcloud) gem instead.)
|
119
|
+
|
120
|
+
### Generating an access token
|
121
|
+
|
122
|
+
To use the `pcloud_api` client, you will need to first generate an access token. You may do this by cloning and running the script in `./bin/generate_access_token`. It will take you through the process of setting up a pCloud app and completing the OAuth2 flow in the browser.
|
123
|
+
|
124
|
+
## Development
|
125
|
+
|
126
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
127
|
+
|
128
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
129
|
+
|
130
|
+
## Contributing
|
131
|
+
|
132
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jhunschejones/pcloud_api.
|
133
|
+
|
134
|
+
|
135
|
+
## License
|
136
|
+
|
137
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/RELEASEING.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
# Cutting a new release of the pCloud API gem:
|
2
|
+
1. Update the gem version in `lib/pcloud/version.rb`.
|
3
|
+
2. Make any documentation updates.
|
4
|
+
3. Tag a new release in GitHub and summarize the changes since the last release.
|
5
|
+
4. Build the gem locally: `gem build pcloud_api.gemspec`.
|
6
|
+
* To test the gem in a local project, you can install it with `gem install --local pcloud_api-<VERSION>.gem`
|
7
|
+
5. Publish the gem to rubygems.org: `gem push pcloud_api-<VERSION>.gem`.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "pcloud_api"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "uri"
|
5
|
+
require "net/http"
|
6
|
+
|
7
|
+
class String
|
8
|
+
def green; "\033[32m#{self}\033[0m" end
|
9
|
+
def cyan; "\033[36m#{self}\033[0m" end
|
10
|
+
end
|
11
|
+
|
12
|
+
puts "1. Register an app at `https://docs.pcloud.com/my_apps/`".cyan
|
13
|
+
|
14
|
+
|
15
|
+
puts "2. Enter the client id and secret for the app:".cyan
|
16
|
+
print "Client ID > "
|
17
|
+
client_id = $stdin.gets.chomp.freeze
|
18
|
+
print "Client Secret > "
|
19
|
+
client_secret = $stdin.gets.chomp.freeze
|
20
|
+
|
21
|
+
|
22
|
+
puts "3. Enter the data region of your pCloud account [EU/US]:".cyan
|
23
|
+
print "> "
|
24
|
+
region_specific_api_base = $stdin.gets.chomp == "EU" ? "eapi.pcloud.com" : "api.pcloud.com"
|
25
|
+
|
26
|
+
|
27
|
+
puts "4. Navigate to this URL to start the access code flow:".cyan
|
28
|
+
puts "`https://my.pcloud.com/oauth2/authorize?client_id=#{client_id}&response_type=code`"
|
29
|
+
|
30
|
+
|
31
|
+
puts "5. After logging in, enter the access code provided below:".cyan
|
32
|
+
print "> "
|
33
|
+
access_code = $stdin.gets.chomp.freeze
|
34
|
+
|
35
|
+
|
36
|
+
puts "6. Requesting access token from pCloud...".cyan
|
37
|
+
query = { client_id: client_id, client_secret: client_secret, code: access_code }
|
38
|
+
uri = URI.parse("https://#{region_specific_api_base}/oauth2_token?#{URI.encode_www_form(query)}")
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
40
|
+
http.use_ssl = true
|
41
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
42
|
+
request["Accept"] = "application/json"
|
43
|
+
response = http.request(request)
|
44
|
+
json_response = JSON.parse(response.body)
|
45
|
+
raise json_response["error"] if json_response["error"]
|
46
|
+
puts "Done! Your access token is: #{json_response["access_token"]}".green
|
data/bin/setup
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
module Pcloud
|
2
|
+
module Client
|
3
|
+
class ErrorResponse < StandardError; end
|
4
|
+
class ConfigurationError < StandardError; end
|
5
|
+
|
6
|
+
VALID_DATA_REGIONS = [
|
7
|
+
EU_DATA_REGION = "EU".freeze,
|
8
|
+
US_DATA_REGION = "US".freeze
|
9
|
+
].freeze
|
10
|
+
US_API_BASE = "api.pcloud.com".freeze
|
11
|
+
EU_API_BASE = "eapi.pcloud.com".freeze
|
12
|
+
TIMEOUT_SECONDS = ENV.fetch("PCLOUD_API_TIMEOUT_SECONDS", "8").to_i.freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def configure(access_token: nil, data_region: nil)
|
16
|
+
@@access_token = access_token
|
17
|
+
@@data_region = data_region
|
18
|
+
true # Don't accidentally return any secrets from the configure method
|
19
|
+
end
|
20
|
+
|
21
|
+
def execute(method, query: {}, body: {})
|
22
|
+
verb = ["uploadfile"].include?(method) ? :post : :get
|
23
|
+
options = {
|
24
|
+
headers: { "Authorization" => "Bearer #{access_token}" },
|
25
|
+
timeout: TIMEOUT_SECONDS # this sets both the open and read timeouts to the same value
|
26
|
+
}
|
27
|
+
options.merge!({ query: query }) unless query.empty?
|
28
|
+
options.merge!({ body: body }) unless body.empty?
|
29
|
+
response = HTTParty.public_send(verb, "https://#{closest_server}/#{method}", options)
|
30
|
+
json_response = JSON.parse(response.body)
|
31
|
+
raise ErrorResponse.new(json_response["error"]) if json_response["error"]
|
32
|
+
json_response
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def data_region
|
38
|
+
@@data_region ||= ENV["PCLOUD_API_DATA_REGION"]
|
39
|
+
return @@data_region if VALID_DATA_REGIONS.include?(@@data_region)
|
40
|
+
raise ConfigurationError.new("Missing pCloud data region") if @@data_region.nil?
|
41
|
+
raise ConfigurationError.new("Invalid pCloud data region, must be one of #{VALID_DATA_REGIONS}")
|
42
|
+
end
|
43
|
+
|
44
|
+
def access_token
|
45
|
+
@@access_token ||= ENV["PCLOUD_API_ACCESS_TOKEN"]
|
46
|
+
return @@access_token unless @@access_token.nil?
|
47
|
+
raise ConfigurationError.new("Missing bearer token")
|
48
|
+
end
|
49
|
+
|
50
|
+
# You can manually hit "https://<default_server_for_your_region>/getapiserver"
|
51
|
+
# to get some JSON which will tell you if there is a server in your region
|
52
|
+
# closer to you than the default.
|
53
|
+
def closest_server
|
54
|
+
@@closest_server ||= begin
|
55
|
+
return ENV["PCLOUD_API_BASE_URI"] if ENV["PCLOUD_API_BASE_URI"]
|
56
|
+
case data_region
|
57
|
+
when US_DATA_REGION
|
58
|
+
US_API_BASE
|
59
|
+
when EU_DATA_REGION
|
60
|
+
EU_API_BASE
|
61
|
+
else
|
62
|
+
raise ConfigurationError.new("Invalid pCloud data region, must be one of #{VALID_DATA_REGIONS}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Pcloud
|
2
|
+
class File
|
3
|
+
module Parser
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.include ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def parse_one(response)
|
11
|
+
Pcloud::File.new(
|
12
|
+
id: response.dig("metadata", "fileid"),
|
13
|
+
path: response.dig("metadata", "path"),
|
14
|
+
name: response.dig("metadata", "name"),
|
15
|
+
content_type: response.dig("metadata", "contenttype"),
|
16
|
+
category_id: response.dig("metadata", "category"),
|
17
|
+
size: response.dig("metadata", "size"),
|
18
|
+
parent_folder_id: response.dig("metadata", "parentfolderid"),
|
19
|
+
is_deleted: response.dig("metadata", "isdeleted"),
|
20
|
+
created_at: response.dig("metadata", "created"),
|
21
|
+
modified_at: response.dig("metadata", "modified")
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_many(response)
|
26
|
+
response["metadata"].map do |metadata|
|
27
|
+
Pcloud::File.new(
|
28
|
+
id: metadata["fileid"],
|
29
|
+
path: metadata["path"],
|
30
|
+
name: metadata["name"],
|
31
|
+
content_type: metadata["contenttype"],
|
32
|
+
category_id: metadata["category"],
|
33
|
+
size: metadata["size"],
|
34
|
+
parent_folder_id: metadata["parentfolderid"],
|
35
|
+
is_deleted: metadata["isdeleted"],
|
36
|
+
created_at: metadata["created"],
|
37
|
+
modified_at: metadata["modified"]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/pcloud/file.rb
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
module Pcloud
|
2
|
+
class File
|
3
|
+
class UnsuportedUpdateParams < StandardError; end
|
4
|
+
class ManformedUpdateParams < StandardError; end
|
5
|
+
class InvalidParameter < StandardError; end
|
6
|
+
class UploadFailed < StandardError; end
|
7
|
+
include Parser
|
8
|
+
|
9
|
+
SUPPORTED_UPDATE_PARAMS = [:name, :parent_folder_id, :path].freeze
|
10
|
+
FILE_CATAGORIES = {
|
11
|
+
"0" => "uncategorized",
|
12
|
+
"1" => "image",
|
13
|
+
"2" => "video",
|
14
|
+
"3" => "audio",
|
15
|
+
"4" => "document",
|
16
|
+
"5" => "archive",
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
attr_reader :id, :path, :name, :content_type, :category, :size,
|
20
|
+
:parent_folder_id, :is_deleted, :created_at, :modified_at
|
21
|
+
|
22
|
+
def initialize(params)
|
23
|
+
@id = params.fetch(:id)
|
24
|
+
@path = params.fetch(:path)
|
25
|
+
@name = params.fetch(:name)
|
26
|
+
@content_type = params.fetch(:content_type)
|
27
|
+
@category = FILE_CATAGORIES.fetch((params.fetch(:category_id) || 0).to_s)
|
28
|
+
@size = params.fetch(:size) # bytes
|
29
|
+
@parent_folder_id = params.fetch(:parent_folder_id)
|
30
|
+
@is_deleted = params.fetch(:is_deleted) || false
|
31
|
+
@created_at = params.fetch(:created_at)
|
32
|
+
@modified_at = params.fetch(:modified_at)
|
33
|
+
end
|
34
|
+
|
35
|
+
def update(params)
|
36
|
+
unless (params.keys - SUPPORTED_UPDATE_PARAMS).empty?
|
37
|
+
raise UnsuportedUpdateParams.new("Must be one of #{SUPPORTED_UPDATE_PARAMS}")
|
38
|
+
end
|
39
|
+
if params[:path] && is_invalid_path_param?(params[:path])
|
40
|
+
raise ManformedUpdateParams.new("`path` param must start and end with `/`")
|
41
|
+
end
|
42
|
+
query = {
|
43
|
+
fileid: id,
|
44
|
+
tofolderid: params[:parent_folder_id] || nil,
|
45
|
+
toname: params[:name] || nil,
|
46
|
+
topath: params[:path] || nil
|
47
|
+
}.compact
|
48
|
+
parse_one(Client.execute("renamefile", query: query))
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete
|
52
|
+
parse_one(Client.execute("deletefile", query: { fileid: id }))
|
53
|
+
end
|
54
|
+
|
55
|
+
def parent_folder
|
56
|
+
@parent_folder ||= Pcloud::Folder.find(parent_folder_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def download_url
|
60
|
+
@download_url ||= begin
|
61
|
+
file_url_parts = Client.execute(
|
62
|
+
"getfilelink",
|
63
|
+
query: { fileid: id, forcedownload: 1, skipfilename: 1 }
|
64
|
+
)
|
65
|
+
"https://#{file_url_parts["hosts"].first}#{file_url_parts["path"]}"
|
66
|
+
end
|
67
|
+
# This allows us to cache the expensive part of this method, requesting
|
68
|
+
# a download URL from pcloud, while maintaining consistency if the file
|
69
|
+
# name changes later.
|
70
|
+
"#{@download_url}/#{URI.encode_www_form_component(name)}"
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def is_invalid_path_param?(path_param)
|
76
|
+
# Path params have to start and end with `/``
|
77
|
+
[path_param[0], path_param[-1]] != ["/", "/"]
|
78
|
+
end
|
79
|
+
|
80
|
+
class << self
|
81
|
+
def find(id)
|
82
|
+
parse_one(Client.execute("stat", query: { fileid: id }))
|
83
|
+
end
|
84
|
+
|
85
|
+
def find_by(path:)
|
86
|
+
parse_one(Client.execute("stat", query: { path: path }))
|
87
|
+
end
|
88
|
+
|
89
|
+
def upload(filename:, file:, path: nil, folder_id: nil)
|
90
|
+
process_upload(
|
91
|
+
filename: filename,
|
92
|
+
file: file,
|
93
|
+
path: path,
|
94
|
+
folder_id: folder_id
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def upload!(filename:, file:, path: nil, folder_id: nil)
|
99
|
+
process_upload(
|
100
|
+
filename: filename,
|
101
|
+
file: file,
|
102
|
+
path: path,
|
103
|
+
folder_id: folder_id,
|
104
|
+
overwrite: true
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def process_upload(filename:, file:, path: nil, folder_id: nil, overwrite: false)
|
111
|
+
raise InvalidParameter.new("The `file` parameter must be an instance of Ruby `File`") unless file.is_a?(::File)
|
112
|
+
|
113
|
+
# === pCloud API behavior notes: ===
|
114
|
+
# 1. If neither `path` nor `folder_id` is provided, the file will be
|
115
|
+
# uploaded into the users root directory by default.
|
116
|
+
# 2. If the `filename` does not match the name of the `file` provided,
|
117
|
+
# pCloud will use the name of the `file` rather than renaming it.
|
118
|
+
response = Client.execute(
|
119
|
+
"uploadfile",
|
120
|
+
body: {
|
121
|
+
renameifexists: overwrite ? 0 : 1,
|
122
|
+
path: path,
|
123
|
+
folderid: folder_id,
|
124
|
+
filename: filename,
|
125
|
+
file: file
|
126
|
+
}.compact,
|
127
|
+
)
|
128
|
+
# This method on the pCloud API can accept multiple uploads at once.
|
129
|
+
# For now, this upload interface just takes a single file at a time
|
130
|
+
# so we return just one file out of this method.
|
131
|
+
uploaded_file = parse_many(response).first
|
132
|
+
raise UploadFailed if uploaded_file.nil?
|
133
|
+
return uploaded_file
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Pcloud
|
2
|
+
class Folder
|
3
|
+
module Parser
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.include ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def parse_one(response)
|
11
|
+
Pcloud::Folder.new(
|
12
|
+
id: response.dig("metadata", "folderid"),
|
13
|
+
path: response.dig("metadata", "path"),
|
14
|
+
name: response.dig("metadata", "name"),
|
15
|
+
parent_folder_id: response.dig("metadata", "parentfolderid"),
|
16
|
+
is_deleted: response.dig("metadata", "isdeleted"),
|
17
|
+
created_at: response.dig("metadata", "created"),
|
18
|
+
modified_at: response.dig("metadata", "modified"),
|
19
|
+
contents: (response.dig("metadata", "contents") || []).map do |content_item|
|
20
|
+
if content_item["isfolder"]
|
21
|
+
Pcloud::Folder.new(
|
22
|
+
id: content_item["folderid"],
|
23
|
+
path: content_item["path"], # no path comes back from this api
|
24
|
+
name: content_item["name"],
|
25
|
+
parent_folder_id: content_item["parentfolderid"],
|
26
|
+
contents: content_item["contents"], # no content comes back from this api
|
27
|
+
is_deleted: content_item["isdeleted"],
|
28
|
+
created_at: content_item["created"],
|
29
|
+
modified_at: content_item["modified"]
|
30
|
+
)
|
31
|
+
else
|
32
|
+
Pcloud::File.new(
|
33
|
+
id: content_item["fileid"],
|
34
|
+
path: content_item["path"],
|
35
|
+
name: content_item["name"],
|
36
|
+
content_type: content_item["contenttype"],
|
37
|
+
category_id: content_item["category"],
|
38
|
+
size: content_item["size"],
|
39
|
+
parent_folder_id: content_item["parentfolderid"],
|
40
|
+
is_deleted: content_item["isdeleted"],
|
41
|
+
created_at: content_item["created"],
|
42
|
+
modified_at: content_item["modified"]
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Pcloud
|
2
|
+
class Folder
|
3
|
+
class UnsuportedUpdateParams < StandardError; end
|
4
|
+
class ManformedUpdateParams < StandardError; end
|
5
|
+
class InvalidCreateParams < StandardError; end
|
6
|
+
include Parser
|
7
|
+
|
8
|
+
SUPPORTED_UPDATE_PARAMS = [:name, :parent_folder_id, :path].freeze
|
9
|
+
|
10
|
+
attr_reader :id, :path, :name, :parent_folder_id, :is_deleted, :created_at,
|
11
|
+
:modified_at
|
12
|
+
|
13
|
+
def initialize(params)
|
14
|
+
@id = params.fetch(:id)
|
15
|
+
@path = params.fetch(:path)
|
16
|
+
@name = params.fetch(:name)
|
17
|
+
@parent_folder_id = params.fetch(:parent_folder_id)
|
18
|
+
@contents = params.fetch(:contents)
|
19
|
+
# Some APIs (mainly recursive operations according to pCloud) return either a
|
20
|
+
# nil or an empty array of contents. In these cases, the @contents_are_confirmed
|
21
|
+
# flag is set to `false` in order to allow one retry to fetch the actual
|
22
|
+
# contents if the `contents` method is called on a folder object that does not
|
23
|
+
# have any contents set yet.
|
24
|
+
@contents_are_confirmed = @contents && @contents.size > 0
|
25
|
+
@is_deleted = params.fetch(:is_deleted) || false
|
26
|
+
@created_at = params.fetch(:created_at)
|
27
|
+
@modified_at = params.fetch(:modified_at)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(params)
|
31
|
+
unless (params.keys - SUPPORTED_UPDATE_PARAMS).empty?
|
32
|
+
raise UnsuportedUpdateParams.new("Must be one of #{SUPPORTED_UPDATE_PARAMS}")
|
33
|
+
end
|
34
|
+
if params[:path] && is_invalid_path_param?(params[:path])
|
35
|
+
raise ManformedUpdateParams.new("`path` param must start and end with `/`")
|
36
|
+
end
|
37
|
+
query = {
|
38
|
+
folderid: id,
|
39
|
+
tofolderid: params[:parent_folder_id] || nil,
|
40
|
+
toname: params[:name] || nil,
|
41
|
+
topath: params[:path] || nil
|
42
|
+
}.compact
|
43
|
+
parse_one(Client.execute("renamefolder", query: query))
|
44
|
+
end
|
45
|
+
|
46
|
+
# This method is the safest way to delte folders and will fail if the folder
|
47
|
+
# has contents.
|
48
|
+
def delete
|
49
|
+
parse_one(Client.execute("deletefolder", query: { folderid: id }))
|
50
|
+
end
|
51
|
+
|
52
|
+
# This method will delete a folder and recursively delete all its contents
|
53
|
+
def delete!
|
54
|
+
Client.execute("deletefolderrecursive", query: { folderid: id })
|
55
|
+
true # we don't get anything helpful back from pCloud on this request
|
56
|
+
end
|
57
|
+
|
58
|
+
def parent_folder
|
59
|
+
@parent_folder ||= Folder.find(parent_folder_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Some APIs return `nil` or `[]` for contents when a folder does actually
|
63
|
+
# have contents. This method allows us to re-try one time if we try to get
|
64
|
+
# the contents and find that they're missing.
|
65
|
+
def contents
|
66
|
+
return @contents if @contents_are_confirmed
|
67
|
+
@contents = Folder.find(id).contents
|
68
|
+
@contents_are_confirmed = true
|
69
|
+
@contents
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def is_invalid_path_param?(path_param)
|
75
|
+
# Path params have to start and end with `/``
|
76
|
+
[path_param[0], path_param[-1]] != ["/", "/"]
|
77
|
+
end
|
78
|
+
|
79
|
+
class << self
|
80
|
+
def first_or_create(params)
|
81
|
+
if params[:parent_folder_id] && params[:name]
|
82
|
+
parse_one(Client.execute("createfolderifnotexists", query: { folderid: params[:parent_folder_id], name: params[:name] }))
|
83
|
+
elsif params[:path]
|
84
|
+
parse_one(Client.execute("createfolderifnotexists", query: { path: params[:path] }))
|
85
|
+
else
|
86
|
+
raise InvalidCreateParams.new("first_or_create must be called with either `path` or both `parent_folder_id` and `name` params")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def find(id)
|
91
|
+
parse_one(Client.execute("listfolder", query: { folderid: id }))
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_by(path:)
|
95
|
+
parse_one(Client.execute("listfolder", query: { path: path }))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/pcloud_api.rb
ADDED
data/pcloud_api.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative 'lib/pcloud/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "pcloud_api"
|
5
|
+
spec.version = Pcloud::VERSION
|
6
|
+
spec.authors = ["Joshua Hunsche Jones"]
|
7
|
+
spec.email = ["joshua@hunschejones.com"]
|
8
|
+
|
9
|
+
spec.summary = "A Ruby library for interacting with the pCloud API"
|
10
|
+
spec.homepage = "https://github.com/jhunschejones/pcloud_api"
|
11
|
+
spec.license = "MIT"
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
13
|
+
|
14
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
15
|
+
spec.metadata["source_code_uri"] = "https://github.com/jhunschejones/pcloud_api"
|
16
|
+
spec.metadata["changelog_uri"] = "https://github.com/jhunschejones/pcloud_api/blob/master/CHANGELOG.md"
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
|
28
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "pry", "~> 0.13"
|
31
|
+
|
32
|
+
spec.add_dependency "httparty", "~> 0.16"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pcloud_api
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joshua Hunsche Jones
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-09-27 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.17'
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.17'
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '12.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '12.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pry
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.13'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0.13'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: httparty
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0.16'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0.16'
|
89
|
+
description:
|
90
|
+
email:
|
91
|
+
- joshua@hunschejones.com
|
92
|
+
executables: []
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- ".gitignore"
|
97
|
+
- ".rspec"
|
98
|
+
- ".travis.yml"
|
99
|
+
- CHANGELOG.md
|
100
|
+
- Gemfile
|
101
|
+
- LICENSE.txt
|
102
|
+
- README.md
|
103
|
+
- RELEASEING.md
|
104
|
+
- Rakefile
|
105
|
+
- bin/console
|
106
|
+
- bin/generate_access_token
|
107
|
+
- bin/setup
|
108
|
+
- lib/pcloud/client.rb
|
109
|
+
- lib/pcloud/file.rb
|
110
|
+
- lib/pcloud/file/parser.rb
|
111
|
+
- lib/pcloud/folder.rb
|
112
|
+
- lib/pcloud/folder/parser.rb
|
113
|
+
- lib/pcloud/version.rb
|
114
|
+
- lib/pcloud_api.rb
|
115
|
+
- pcloud_api.gemspec
|
116
|
+
homepage: https://github.com/jhunschejones/pcloud_api
|
117
|
+
licenses:
|
118
|
+
- MIT
|
119
|
+
metadata:
|
120
|
+
homepage_uri: https://github.com/jhunschejones/pcloud_api
|
121
|
+
source_code_uri: https://github.com/jhunschejones/pcloud_api
|
122
|
+
changelog_uri: https://github.com/jhunschejones/pcloud_api/blob/master/CHANGELOG.md
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 2.3.0
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubygems_version: 3.2.28
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: A Ruby library for interacting with the pCloud API
|
142
|
+
test_files: []
|