synology 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 691973268b923bf903f848351db9525ed49d3b856e844a98dd705cc6d0d135f4
4
+ data.tar.gz: 8098dcffd55f22c2ed5b1c614109bdf7e1b2b4c123010c46dfd9a60521553c03
5
+ SHA512:
6
+ metadata.gz: d512713deba8261ea8eb57f7db8c94ac4ab15ec08d9973f4df726df168adf82530c5ecf5ff135833e68104de0ed86fc7c1a9ae85d9c63558b806e5595408e2aa
7
+ data.tar.gz: '08d006a8c22509960393172d0d640a4958aaa135ffb085260476434b5b45446d66511ecd8ff986124d8d15a72df9ed40dc8cb5d9aa8d8a4f8206979d64667962'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Henry Maestu
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,166 @@
1
+ # Synology
2
+
3
+ Synology NAS API client written in Ruby.
4
+
5
+ Full implantation of Synology API based on this documentation: https://global.download.synology.com/download/Document/Software/DeveloperGuide/Package/FileStation/All/enu/Synology_File_Station_API_Guide.pdf
6
+
7
+ ## Installation
8
+
9
+ ```ruby
10
+ gem 'synology'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+
18
+ ## Usage
19
+
20
+ Have some other Synology Service exposed define endpoints in config.
21
+ ```
22
+ Synology.configure do |config|
23
+
24
+ ....
25
+ # Define your own endpoints
26
+ # May break code with POST request...
27
+ # Example: lib/synology/client.rb
28
+ config.api_endpoint = {
29
+ info: { api: 'SYNO.FileStation.Info', version: 2, method: 'get', http_method: 'GET' },
30
+ # SYNO.API.Auth
31
+ login: { api: 'SYNO.API.Auth', version: 3, method: 'login', http_method: 'GET' },
32
+ logout: { api: 'SYNO.API.Auth', version: 1, method: 'logout', http_method: 'GET' }
33
+ ...
34
+ }
35
+ end
36
+ ```
37
+
38
+ ### Basic usages
39
+
40
+ ```
41
+ Synology.configure do |config|
42
+ config.host = 'https://finds.synology.com'
43
+ config.username = 'admin'
44
+ config.password = 'password'
45
+ config.https = true
46
+ end
47
+
48
+ client = Synology::Client.new
49
+ # Login
50
+ client.login(account: client.username, passwd: client.password)
51
+
52
+ # List files in folder
53
+ client.list_folder(folder_path: '/my_folder', additional: '["real_path", "size", "owner"]')
54
+
55
+ # Upload a file
56
+ client.upload(path: '/my_folder', file: {file_name:'testing', file_content: 'Testing123'})
57
+ # Download a file body Base64.strict_encoded
58
+ client.download(path: '/my_folder/testing')
59
+
60
+ client.close
61
+
62
+ ```
63
+
64
+ Authentication Methods
65
+
66
+ client.login(account: , passwd:)# Authenticate with Synology NAS
67
+ client.logout # End the current session
68
+
69
+ #### File and Folder Operations
70
+
71
+ List & Info
72
+
73
+ client.list_share # List all shared folders
74
+ client.list_folder(folder_path:, additional:) # List contents of a folder
75
+ client.list_get_info # Get information about files/folders
76
+
77
+ Search Operations
78
+
79
+ client.search_start(folder_path:, pattern:) # Start a search operation
80
+ client.search_list(taskid:) # List search results
81
+ client.search_stop(taskid:) # Stop a search operation
82
+ client.search_clean(taskid:) # Clean up search results
83
+
84
+ Virtual Folder Operations
85
+
86
+ client.list_all_mount_points(type:, additional:) # List mount points of virtual file systems
87
+
88
+ Favorite Management
89
+
90
+ client.favorite_list # List favorite folders
91
+ client.favorite_add(path:, name:) # Add folder to favorites
92
+ client.favorite_edit(path:, name:) # Edit favorite folder
93
+ client.favorite_delete(path:) # Remove from favorites
94
+ client.favorite_clear_broken # Clear broken favorite links
95
+
96
+ Thumbnail Operations
97
+
98
+ client.get_thumbnail(path:) # Get thumbnail of a file
99
+
100
+ Directory Operations
101
+
102
+ client.dir_start(path:) # Start directory size calculation
103
+ client.dir_status(taskid:) # Check directory size calculation status
104
+ client.dir_stop(taskid:) # Stop directory size calculation
105
+
106
+ MD5 Operations
107
+
108
+ client.md5_start(file_path:) # Start MD5 calculation
109
+ client.md5_status(taskid:) # Check MD5 calculation status
110
+ client.md5_stop(taskid:) # Stop MD5 calculation
111
+
112
+ #### File Management
113
+
114
+ Basic Operations
115
+
116
+ client.check_permission(path:, filename:) # Check file/folder permissions
117
+ client.upload(path:, file:{file_name:, file_content:}) # Upload files
118
+ client.download(path:) # Download files
119
+ client.create_folder(folder_path:, name:) # Create new folder
120
+ client.rename(path:, name:) # Rename file/folder
121
+ client.delete(path:) # Delete file/folder immediately
122
+
123
+ Sharing Operations
124
+
125
+ client.sharing_get_info(id:) # Get sharing link info
126
+ client.sharing_links_list # List all sharing links
127
+ client.sharing_link_create(path:) # Create sharing link
128
+ client.sharing_link_delete(id:) # Delete sharing link
129
+ client.sharing_link_edit(id:) # Edit sharing link
130
+ client.sharing_link_clear_invalid # Clear invalid sharing links
131
+
132
+ Asynchronous Operations
133
+
134
+ client.copy_move_start(path:, dest_folder_path:) # Start copy/move operation
135
+ client.copy_move_status(taskid:) # Check copy/move status
136
+ client.copy_move_stop(taskid:) # Stop copy/move operation
137
+
138
+ Delete Operations
139
+
140
+ client.delete_async_start(path:) # Start asynchronous delete
141
+ client.delete_async_status(taskid:) # Check delete status
142
+ client.delete_async_stop(taskid:) # Stop delete operation
143
+
144
+ Archive Operations
145
+
146
+ client.extract_start(file_path:, dest_folder_path:) # Start extraction
147
+ client.extract_status(taskid:) # Check extraction status
148
+ client.extract_stop(taskid:) # Stop extraction
149
+ client.extract_list(file_path:) # List archive contents
150
+ client.compress_start(path:, dest_file_path:) # Start compression
151
+ client.compress_status(taskid:) # Check compression status
152
+ client.compress_stop(taskid:) # Stop compression
153
+ client.compress_list(file_path:) # List compressed file contents
154
+
155
+ Background Tasks
156
+
157
+ client.background_task_list # List background tasks
158
+ client.background_task_clear_finished(taskid:) # Clear finished background tasks
159
+
160
+ ## Contributing
161
+
162
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dragonwebeu/synology.
163
+
164
+ ## License
165
+
166
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'openssl'
7
+ require 'base64'
8
+
9
+ module Synology
10
+ class Client
11
+ API_ENDPOINT = 'webapi/entry.cgi'
12
+
13
+ # API endpoints definition using metaprogramming
14
+ # https://global.download.synology.com/download/Document/Software/DeveloperGuide/Package/FileStation/All/enu/Synology_File_Station_API_Guide.pdf
15
+ API_ENDPOINTS = {
16
+ info: { api: 'SYNO.FileStation.Info', version: 2, method: 'get', http_method: 'GET' },
17
+ # SYNO.API.Auth
18
+ login: { api: 'SYNO.API.Auth', version: 3, method: 'login', http_method: 'GET' },
19
+ logout: { api: 'SYNO.API.Auth', version: 1, method: 'logout', http_method: 'GET' },
20
+ # SYNO.FileStation.List
21
+ list: {
22
+ #client.list_share
23
+ share: { api: 'SYNO.FileStation.List', version: 2, method: 'list_share', http_method: 'GET' },
24
+ # client.list_folder(folder_path: '/my_folder', additional: '["real_path", "size", "owner"]')
25
+ folder: { api: 'SYNO.FileStation.List', version: 2, method: 'list', http_method: 'GET' },
26
+ get_info: { api: 'SYNO.FileStation.List', version: 2, method: 'getinfo', http_method: 'GET' }
27
+ },
28
+ # SYNO.FileStation.Search
29
+ search: {
30
+ # client.search_start(folder_path: '/my_folder', pattern: 'test')
31
+ start: { api: 'SYNO.FileStation.Search', version: 2, method: 'start', http_method: 'GET' },
32
+ # client.search_list(taskid: '173126991043E83CB8')
33
+ list: { api: 'SYNO.FileStation.Search', version: 2, method: 'list', http_method: 'GET' },
34
+ # client.search_stop(taskid: '173126991043E83CB8')
35
+ stop: { api: 'SYNO.FileStation.Search', version: 2, method: 'stop', http_method: 'GET' },
36
+ # client.search_clean(taskid: '173126991043E83CB8')
37
+ clean: { api: 'SYNO.FileStation.Search', version: 2, method: 'clean', http_method: 'GET' }
38
+ },
39
+ # SYNO.FileStation.VirtualFolder
40
+ # List all mount point folders of virtual file system, e.g., CIFS or ISO
41
+ # client.list_all_mount_points(type: 'cifs', additional: '["real_path","owner","time","perm","mount_point_type","volume_status"]')
42
+ list_all_mount_points: { api: 'SYNO.FileStation.VirtualFolder', version: 2, method: 'list', http_method: 'GET' },
43
+ favorite: {
44
+ # client.favorite_list
45
+ list: { api: 'SYNO.FileStation.Favorite', version: 2, method: 'list', http_method: 'GET' },
46
+ # client.favorite_add(path: '/my_folder', name: 'Henry')
47
+ add: { api: 'SYNO.FileStation.Favorite', version: 2, method: 'add', http_method: 'GET' },
48
+ # client.favorite_edit(path: '/my_folder', name: 'Henry2')
49
+ edit: { api: 'SYNO.FileStation.Favorite', version: 2, method: 'add', http_method: 'GET' },
50
+ # client.favorite_delete(path: '/my_folder')
51
+ delete: { api: 'SYNO.FileStation.Favorite', version: 2, method: 'delete', http_method: 'GET' },
52
+ # client.favorite_clear_broken
53
+ clear_broken: { api: 'SYNO.FileStation.Favorite', version: 2, method: 'clear_broken', http_method: 'GET' }
54
+ },
55
+ # SYNO.FileStation.Thumb
56
+ # Get a thumbnail of a file.
57
+ # client.get_thumbnail(path:'/my_folder')
58
+ get_thumbnail: { api: 'SYNO.FileStation.Thumb', version: 2, method: 'get', http_method: 'GET' },
59
+ # SYNO.FileStation.DirSize
60
+ dir: {
61
+ # client.dir_start(path: '/my_folder')
62
+ start: { api: 'SYNO.FileStation.DirSize', version: 2, method: 'start', http_method: 'GET' },
63
+ # client.dir_status(taskid: '1731273375F0B260B3')
64
+ status: { api: 'SYNO.FileStation.DirSize', version: 2, method: 'status', http_method: 'GET' },
65
+ # client.dir_stop(taskid: '1731273375F0B260B3')
66
+ stop: { api: 'SYNO.FileStation.DirSize', version: 2, method: 'status', http_method: 'GET' }
67
+ },
68
+ # SYNO.FileStation.MD5
69
+ md5: {
70
+ # client.md5_start(file_path: '/my_folder/some_move.mov')
71
+ start: { api: 'SYNO.FileStation.MD5', version: 2, method: 'start', http_method: 'GET' },
72
+ # client.md5_status(taskid: '1731274183A791CF18')
73
+ status: { api: 'SYNO.FileStation.MD5', version: 2, method: 'status', http_method: 'GET' },
74
+ # client.md5_stop(taskid: '1731274183A791CF18')
75
+ stop: { api: 'SYNO.FileStation.MD5', version: 2, method: 'stop', http_method: 'GET' }
76
+ },
77
+ # SYNO.FileStation.CheckPermission
78
+ # client.check_permission(path: '', filename: '')
79
+ check_permission: { api: 'SYNO.FileStation.CheckPermission', version: 3, method: 'write', http_method: 'GET' },
80
+ #SYNO.FileStation.Upload
81
+ # upload = client.upload(path: '/my_folder', file: {file_name:'testing', file_content: 'Testing123'})
82
+ upload: { api: 'SYNO.FileStation.Upload', version: 2, method: 'upload', http_method: 'POST' }, # Only POST method in the API
83
+ # download_file = client.download(path: '/my_folder/testing')
84
+ download: { api: 'SYNO.FileStation.Download', version: 2, method: 'download', http_method: 'GET' },
85
+ # SYNO.FileStation.Sharing
86
+ sharing: {
87
+ # sharing_link = client.sharing_get_info(id: '12345')
88
+ get_info: { api: 'SYNO.FileStation.Sharing', version: 3, method: 'getinfo', http_method: 'GET' },
89
+ # client.sharing_links_list
90
+ links_list: { api: 'SYNO.FileStation.Sharing', version: 3, method: 'list', http_method: 'GET' },
91
+ # client.sharing_link_create(path: '/my_folder')
92
+ link_create: { api: 'SYNO.FileStation.Sharing', version: 3, method: 'create', http_method: 'GET' },
93
+ # client.sharing_link_delete(id: '12345')
94
+ link_delete: { api: 'SYNO.FileStation.Sharing', version: 3, method: 'delete', http_method: 'GET' },
95
+ # # client.sharing_link_edit(id: '12345')
96
+ link_edit: { api: 'SYNO.FileStation.Sharing', version: 3, method: 'edit', http_method: 'GET' },
97
+ # client.sharing_link_clear_invalid
98
+ link_clear_invalid: { api: 'SYNO.FileStation.Sharing', version: 3, method: 'clear_invalid', http_method: 'GET' }
99
+ },
100
+ # SYNO.FileStation.CreateFolder
101
+ # client.create_folder(folder_path: '/my_folder', name: 'testing1')
102
+ create_folder: { api: 'SYNO.FileStation.CreateFolder', version: 2, method: 'create', http_method: 'GET' },
103
+ # SYNO.FileStation.Rename
104
+ # client.rename(path: '/my_folder/Testing1', name: 'Testing2')
105
+ rename: { api: 'SYNO.FileStation.Rename', version: 2, method: 'rename', http_method: 'GET' },
106
+ # SYNO.FileStation.CopyMove
107
+ copy_move: {
108
+ # client.copy_move_start(path: '/my_folder/testing', dest_folder_path: '/my_folder/Testing2')
109
+ start: { api: 'SYNO.FileStation.CopyMove', version: 3, method: 'start', http_method: 'GET' },
110
+ # client.copy_move_status(taskid: 'FileStation_51D00B7912CDE0B0')
111
+ status: { api: 'SYNO.FileStation.CopyMove', version: 3, method: 'status', http_method: 'GET' },
112
+ # client.copy_move_stop(taskid: 'FileStation_51D00B7912CDE0B0')
113
+ stop: { api: 'SYNO.FileStation.CopyMove', version: 3, method: 'stop', http_method: 'GET' },
114
+ },
115
+ # SYNO.FileStation.Delete
116
+ #
117
+ delete_async: {
118
+ # client.delete_async_start(path: '/my_folder/testing')
119
+ start: { api: 'SYNO.FileStation.Delete', version: 2, method: 'start', http_method: 'GET' },
120
+ # client.delete_async_status(taskid: 'FileStation_51CEC9C979340E5A')
121
+ status: { api: 'SYNO.FileStation.Delete', version: 2, method: 'status', http_method: 'GET' },
122
+ # client.delete_async_stop(taskid: 'FileStation_51CEC9C979340E5A')
123
+ stop: { api: 'SYNO.FileStation.Delete', version: 2, method: 'stop', http_method: 'GET' }
124
+ },
125
+ # client.delete(path: '/my_folder/testing')
126
+ delete: { api: 'SYNO.FileStation.Delete', version: 2, method: 'delete', http_method: 'GET' },
127
+ # SYNO.FileStation.Extract
128
+ extract: {
129
+ # client.extract_start(file_path: '/my_folder/testing.zip', dest_folder_path: '/my_folder')
130
+ start: { api: 'SYNO.FileStation.Extract', version: 2, method: 'start', http_method: 'GET' },
131
+ # client.extract_status(taskid: 'FileStation_51CBB59C68EFE6A3')
132
+ status: { api: 'SYNO.FileStation.Extract', version: 2, method: 'status', http_method: 'GET' },
133
+ # client.extract_stop(taskid: 'FileStation_51CBB59C68EFE6A3')
134
+ stop: { api: 'SYNO.FileStation.Extract', version: 2, method: 'stop', http_method: 'GET' },
135
+ # client.extract_list(file_path: '/my_folder/testing.zip')
136
+ list: { api: 'SYNO.FileStation.Extract', version: 2, method: 'list', http_method: 'GET' }
137
+ },
138
+ #SYNO.FileStation.Compress
139
+ compress: {
140
+ # client.compress_start(path: '/my_folder/testing', dest_file_path: '/my_folder')
141
+ start: { api: 'SYNO.FileStation.Compress', version: 3, method: 'start', http_method: 'GET' },
142
+ # client.compress_status(taskid: 'FileStation_51CBB25CC31961FD')
143
+ status: { api: 'SYNO.FileStation.Compress', version: 3, method: 'status', http_method: 'GET' },
144
+ # client.compress_stop(taskid: 'FileStation_51CBB25CC31961FD')
145
+ stop: { api: 'SYNO.FileStation.Compress', version: 3, method: 'stop', http_method: 'GET' },
146
+ # client.compress_list(file_path: '/my_folder/testing.zip')
147
+ list: { api: 'SYNO.FileStation.Compress', version: 3, method: 'status', http_method: 'GET' }
148
+ },
149
+ background_task: {
150
+ # client.background_task_list
151
+ list: { api: 'SYNO.FileStation.BackgroundTask', version: 3, method: 'list', http_method: 'GET' },
152
+ # client.background_task_clear_finished(taskid: 'FileStation_51D530978633C014')
153
+ clear_finished: { api: 'SYNO.FileStation.BackgroundTask', version: 3, method: 'clear_finished', http_method: 'GET' }
154
+ }
155
+ }
156
+
157
+
158
+ attr_reader :username, :password
159
+ attr_accessor :sid
160
+
161
+ def initialize
162
+ @config = Synology.configuration
163
+ @username = @config.username
164
+ @password = @config.password
165
+ @sid = nil
166
+ @api_endpoint = @config.api_endpoint || API_ENDPOINT
167
+ # API_ENDPOINTS
168
+ @api_endpoints = @config.api_endpoints || API_ENDPOINTS
169
+ generate_api_methods
170
+ end
171
+
172
+ private
173
+
174
+ def generate_api_methods
175
+ @api_endpoints.each do |method_name, endpoints|
176
+ unless endpoints.dig(:api)
177
+ endpoints.each do |method_name2, endpoints2|
178
+ nested_method_name = "#{method_name}_#{method_name2}"
179
+ define_singleton_method(nested_method_name) do |params = {}|
180
+ execute_request(endpoints2, params)
181
+ end
182
+ end
183
+ else
184
+ define_singleton_method(method_name) do |params = {}|
185
+ execute_request(endpoints, params)
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ def execute_request(endpoint_info, params)
192
+ uri = URI("#{@config.base_url}/#{API_ENDPOINT}/#{endpoint_info[:path]}")
193
+ request_params = {
194
+ api: endpoint_info[:api],
195
+ version: endpoint_info[:version],
196
+ method: endpoint_info[:method]
197
+ }.merge(params)
198
+
199
+ # Todo: cleanup or get sid other way
200
+ request_params[:_sid] = @sid if @sid && @use_cookies
201
+
202
+ response = make_request(uri, request_params, endpoint_info[:http_method])
203
+ handle_response(response, request_params)
204
+ end
205
+
206
+ def make_request(uri, params, request_type='GET')
207
+ # Todo: find better solution for this
208
+ if params[:method] == 'upload'
209
+ file = params[:file]
210
+ params.delete(:file)
211
+
212
+ if file[:file_content].nil?
213
+ raise "File content can't be nil!"
214
+ end
215
+ end
216
+ uri.query = URI.encode_www_form(params)
217
+ https = Net::HTTP.new(uri.host, uri.port)
218
+ if @config.https
219
+ https.use_ssl = true
220
+ # https.verify_mode = OpenSSL::SSL::VERIFY_NONE
221
+ end
222
+
223
+ request = Net::HTTP::Get.new(uri) if request_type.upcase == 'GET'
224
+ if request_type.upcase == 'POST'
225
+ request = Net::HTTP::Post.new(uri)
226
+ boundary = rand(100000)
227
+ request.body = upload_body(params, file, boundary).join
228
+ request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
229
+ end
230
+ unless @sid.nil?
231
+ request['Cookie'] = "#{@sid}"
232
+ end
233
+ response = https.request(request)
234
+ if response.header['set-cookie']
235
+ @sid = response.header['set-cookie']
236
+ end
237
+ response
238
+ end
239
+
240
+ def handle_response(response, params)
241
+ # Todo: Cleanup this later and find better solution
242
+ if params[:method] == 'download' && response.code == '200'
243
+ response.body = {body: Base64.strict_encode64(response.body), content_type: response['Content-Type'], success: true}.to_json
244
+ end
245
+ if response.is_a?(Net::HTTPSuccess)
246
+ return parse_json(response.body, params)
247
+ end
248
+
249
+ raise Error, "HTTP Request failed: #{response.code} #{response.message}"
250
+ end
251
+
252
+ def parse_json(body, params)
253
+ json_body = JSON.parse(body)
254
+ # Todo: Change is so it runnable by config settings
255
+ unless json_body['error'].nil?
256
+ Synology::Error.raise_error(json_body['error']['code'], params[:api])
257
+ end
258
+ json_body
259
+ rescue JSON::ParserError
260
+ raise Error, "Invalid JSON response"
261
+ end
262
+
263
+ def upload_body(params, file, boundary)
264
+ post_body = []
265
+ post_body << upload_params_to_form_data(params, boundary)
266
+ post_body << "--#{boundary}\r\n"
267
+ post_body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{file[:file_name]}\"\r\n"
268
+ post_body << "Content-Type: application/octet-stream\r\n\r\n"
269
+ post_body << file[:file_content]
270
+ post_body << "\r\n--#{boundary}--\r\n"
271
+ post_body
272
+ end
273
+
274
+ def upload_params_to_form_data(params, boundary)
275
+ params_body = []
276
+ params.each do |param|
277
+ params_body << "--#{boundary}\r\n"
278
+ params_body << "Content-Disposition: form-data; name=\"#{param[0].to_s}\"\r\n\r\n"
279
+ params_body << "#{param[1].to_s}\r\n"
280
+ end
281
+ params_body
282
+ end
283
+
284
+
285
+ end
286
+ end
@@ -0,0 +1,19 @@
1
+ module Synology
2
+ class Configuration
3
+ attr_accessor :host, :port, :username, :password, :use_cookies, :https, :api_endpoints, :api_endpoint, :parse_errors
4
+
5
+ def initialize
6
+ @api_endpoints = nil
7
+ @api_endpoint = nil
8
+ # Syno default port
9
+ @port = 5000
10
+ @https = false
11
+ @use_cookies = true
12
+ @parse_errors = true
13
+ end
14
+
15
+ def base_url
16
+ "#{https ? 'https' : 'http'}://#{host}:#{port}"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,140 @@
1
+ module Synology
2
+ class Error < StandardError
3
+
4
+ COMMON_ERRORS = {
5
+ 100 => "Unknown error",
6
+ 101 => "No parameter of API, method or version",
7
+ 102 => "The requested API does not exist",
8
+ 103 => "The requested method does not exist",
9
+ 104 => "The requested version does not support the functionality",
10
+ 105 => "The logged in session does not have permission",
11
+ 106 => "Session timeout",
12
+ 107 => "Session interrupted by duplicate login",
13
+ 119 => "SID not found",
14
+ 400 => "Invalid parameter of file operation",
15
+ 401 => "Unknown error of file operation",
16
+ 402 => "System is too busy",
17
+ 403 => "Invalid user does this file operation",
18
+ 404 => "Invalid group does this file operation",
19
+ 405 => "Invalid user and group does this file operation",
20
+ 406 => "Can't get user/group information from the account server",
21
+ 407 => "Operation not permitted",
22
+ 408 => "No such file or directory",
23
+ 409 => "Non-supported file system",
24
+ 410 => "Failed to connect internet-based file system (e.g., CIFS)",
25
+ 411 => "Read-only file system",
26
+ 412 => "Filename too long in the non-encrypted file system",
27
+ 413 => "Filename too long in the encrypted file system",
28
+ 414 => "File already exists",
29
+ 415 => "Disk quota exceeded",
30
+ 416 => "No space left on device",
31
+ 417 => "Input/output error",
32
+ 418 => "Illegal name or path",
33
+ 419 => "Illegal file name",
34
+ 420 => "Illegal file name on FAT file system",
35
+ 421 => "Device or resource busy",
36
+ 599 => "No such task of the file operation"
37
+ }
38
+
39
+ AUTH_ERRORS = {
40
+ 400 => "No such account or incorrect password",
41
+ 401 => "Account disabled",
42
+ 402 => "Permission denied",
43
+ 403 => "2-step verification code required",
44
+ 404 => "Failed to authenticate 2-step verification code"
45
+ }
46
+
47
+ # SYNO.FileStation.Favorite
48
+ FAVORITE_ERRORS = {
49
+ 800 => "A folder path of favorite folder is already added to user's favorites",
50
+ 801 => "A name of favorite folder conflicts with an existing folder path in the user's favorites",
51
+ 802 => "There are too many favorites to be added"
52
+ }
53
+
54
+ UPLOAD_ERRORS = {
55
+ 1800 => "There is no Content-Length information in the HTTP header or the received size doesn't match the value of Content-Length information in the HTTP header",
56
+ 1801 => "Wait too long, no date can be received from client (Default maximum wait time is 3600 seconds)",
57
+ 1802 => "No filename information in the last part of file content",
58
+ 1803 => "Upload connection is cancelled",
59
+ 1804 => "Failed to upload oversized file to FAT file system",
60
+ 1805 => "Can't overwrite or skip the existing file, if no overwrite parameter is given"
61
+ }
62
+
63
+ SHARING_ERRORS = {
64
+ 2000 => "Sharing link does not exist",
65
+ 2001 => "Cannot generate sharing link because too many sharing links exist",
66
+ 2002 => "Failed to access sharing links"
67
+ }
68
+
69
+ CREATE_FOLDER_ERRORS = {
70
+ 1100 => "Failed to create a folder. More information in <errors> object",
71
+ 1101 => "The number of folders to the parent folder would exceed the system limitation"
72
+ }
73
+
74
+ RENAME_ERRORS = {
75
+ 1200 => "Failed to rename it. More information in <errors> object"
76
+ }
77
+
78
+ COPY_MOVE_ERRORS = {
79
+ 1000 => "Failed to copy files/folders. More information in <errors> object",
80
+ 1001 => "Failed to move files/folders. More information in <errors> object",
81
+ 1002 => "An error occurred at the destination. More information in <errors> object",
82
+ 1003 => "Cannot overwrite or skip the existing file because no overwrite parameter is given",
83
+ 1004 => "File cannot overwrite a folder with the same name, or folder cannot overwrite a file with the same name",
84
+ 1006 => "Cannot copy/move file/folder with special characters to a FAT32 file system",
85
+ 1007 => "Cannot copy/move a file bigger than 4G to a FAT32 file system"
86
+ }
87
+
88
+ DELETE_ERRORS = {
89
+ 900 => "Failed to delete file(s)/folder(s). More information in <errors> object"
90
+ }
91
+
92
+ EXTRACT_ERRORS = {
93
+ 1400 => "Failed to extract files",
94
+ 1401 => "Cannot open the file as archive",
95
+ 1402 => "Failed to read archive data error",
96
+ 1403 => "Wrong password",
97
+ 1404 => "Failed to get the file and dir list in an archive",
98
+ 1405 => "Failed to find the item ID in an archive file"
99
+ }
100
+
101
+ COMPRESS_ERRORS = {
102
+ 1300 => "Failed to compress files/folders",
103
+ 1301 => "Cannot create the archive because the given archive name is too long"
104
+ }
105
+
106
+ class << self
107
+
108
+ attr_reader :code
109
+
110
+ def initialize(message = nil, code = nil)
111
+ @code = code
112
+ super(message)
113
+ end
114
+
115
+ def get_error_message(code, operation = nil)
116
+ # Todo: Find cleaner better way, We dont merge because of duplicate keys.
117
+ error_map = case operation
118
+ when 'SYNO.API.Auth' then AUTH_ERRORS
119
+ when 'SYNO.FileStation.Favorite' then FAVORITE_ERRORS.merge(COMMON_ERRORS)
120
+ when 'SYNO.FileStation.Upload' then UPLOAD_ERRORS.merge(COMMON_ERRORS)
121
+ when 'SYNO.FileStation.Sharing' then SHARING_ERRORS.merge(COMMON_ERRORS)
122
+ when 'SYNO.FileStation.CreateFolder' then CREATE_FOLDER_ERRORS.merge(COMMON_ERRORS)
123
+ when 'SYNO.FileStation.Rename' then RENAME_ERRORS.merge(COMMON_ERRORS)
124
+ when 'SYNO.FileStation.CopyMove' then COPY_MOVE_ERRORS.merge(COMMON_ERRORS)
125
+ when 'SYNO.FileStation.Delete' then DELETE_ERRORS.merge(COMMON_ERRORS)
126
+ when 'SYNO.FileStation.Extract' then EXTRACT_ERRORS.merge(COMMON_ERRORS)
127
+ when 'SYNO.FileStation.Compress' then COMPRESS_ERRORS.merge(COMMON_ERRORS)
128
+ else COMMON_ERRORS
129
+ end
130
+
131
+ error_map[code] || COMMON_ERRORS[code] || "Unknown error (Code: #{code})"
132
+ end
133
+
134
+ def raise_error(code, operation = nil)
135
+ message = get_error_message(code, operation)
136
+ raise new("#{message} (Error Code: #{code})")
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synology
4
+ VERSION = "0.1.0"
5
+ end
data/lib/synology.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "synology/configuration"
4
+ require_relative "synology/client"
5
+ require_relative "synology/version"
6
+ require_relative "synology/error"
7
+
8
+ module Synology
9
+ class << self
10
+ attr_accessor :configuration
11
+ end
12
+
13
+ def self.configure
14
+ self.configuration ||= Configuration.new
15
+ yield(configuration) if block_given?
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: synology
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Henry Maestu
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Synology NAS API client written in Ruby
14
+ email:
15
+ - dragonwebeu@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - lib/synology.rb
25
+ - lib/synology/client.rb
26
+ - lib/synology/configuration.rb
27
+ - lib/synology/error.rb
28
+ - lib/synology/version.rb
29
+ homepage: https://github.com/dragonwebeu/synology
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/dragonwebeu/synology
34
+ source_code_uri: https://github.com/dragonwebeu/synology
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.3.1
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 3.0.9
51
+ signing_key:
52
+ specification_version: 4
53
+ summary: Synology API Client in Ruby
54
+ test_files: []