synology 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 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: []