sdls 0.1.0 → 0.1.2
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 +4 -4
- data/lib/sdls/cli.rb +2 -2
- data/lib/sdls/client.rb +36 -15
- data/lib/sdls/config.rb +174 -16
- data/lib/sdls/version.rb +1 -1
- metadata +45 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1649a83c3850e7e6a8657edfa6fce674ca844211d5264ef016dd9ddc6c413e8f
|
4
|
+
data.tar.gz: f826f7983ee0a67f1c1aabaeb6e172df8541f76c63415dbbf028600d479beeef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17ab00c3765268aa845339d54e65caea98c69e791d3f9f1d13345ab6aea5c82bc58576449b0921b1e6d9720163e30c03337ad0e1b6fbd3241b60601fe13b13d1
|
7
|
+
data.tar.gz: 54eca13f2a055533da40642741dd1e897816ea0daed83b59108cc92d53040bdeac0d29ebe3a5cdedf9e4ed19dd43c055113f3ee43a5ef0d5005d2470647f3d9f
|
data/lib/sdls/cli.rb
CHANGED
@@ -48,8 +48,8 @@ module SDLS
|
|
48
48
|
puts " host: #{current_config.host}"
|
49
49
|
puts " username: #{current_config.username}"
|
50
50
|
puts " password: [REDACTED]"
|
51
|
-
puts " op_item_name: #{current_config.op_item_name}"
|
52
|
-
puts " directories: #{current_config.directories.join(", ")}" if current_config.directories
|
51
|
+
puts " op_item_name: #{current_config.op_item_name || "[NOT SET]"}"
|
52
|
+
puts " directories: #{current_config.directories.join(", ")}" if current_config.directories&.any?
|
53
53
|
end
|
54
54
|
|
55
55
|
desc "connect", "Verify connectivity and authentication with the server"
|
data/lib/sdls/client.rb
CHANGED
@@ -2,6 +2,7 @@ require "net/http"
|
|
2
2
|
require "uri"
|
3
3
|
require "json"
|
4
4
|
require "open3"
|
5
|
+
require "tty-prompt"
|
5
6
|
|
6
7
|
module SDLS
|
7
8
|
class Client
|
@@ -13,8 +14,8 @@ module SDLS
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def authenticate(otp: nil)
|
16
|
-
uri =
|
17
|
-
response_body =
|
17
|
+
uri = URI.join(@host, "/webapi/auth.cgi")
|
18
|
+
response_body = make_post_request(uri, auth_params(otp))
|
18
19
|
|
19
20
|
handle_auth_response(response_body, otp)
|
20
21
|
rescue => e
|
@@ -26,7 +27,7 @@ module SDLS
|
|
26
27
|
sid = authenticate
|
27
28
|
return false unless sid
|
28
29
|
|
29
|
-
uri =
|
30
|
+
uri = URI.join(@host, "/webapi/DownloadStation/task.cgi")
|
30
31
|
data = download_params(magnet, destination, sid)
|
31
32
|
|
32
33
|
response = Net::HTTP.post_form(uri, data)
|
@@ -43,14 +44,8 @@ module SDLS
|
|
43
44
|
|
44
45
|
private
|
45
46
|
|
46
|
-
def
|
47
|
-
|
48
|
-
uri.query = URI.encode_www_form(params) if params
|
49
|
-
uri
|
50
|
-
end
|
51
|
-
|
52
|
-
def make_request(uri)
|
53
|
-
response = Net::HTTP.get_response(uri)
|
47
|
+
def make_post_request(uri, params)
|
48
|
+
response = Net::HTTP.post_form(uri, params)
|
54
49
|
raise "HTTP error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
55
50
|
JSON.parse(response.body)
|
56
51
|
end
|
@@ -96,10 +91,31 @@ module SDLS
|
|
96
91
|
end
|
97
92
|
|
98
93
|
def retry_with_otp
|
99
|
-
puts "OTP required for authentication.
|
100
|
-
|
101
|
-
|
102
|
-
|
94
|
+
puts "OTP required for authentication."
|
95
|
+
|
96
|
+
# Try 1Password first if configured and available
|
97
|
+
if @op_item_name && onepassword_cli_available?
|
98
|
+
puts "Fetching OTP from 1Password..."
|
99
|
+
fetched_otp = fetch_otp_from_1password
|
100
|
+
return authenticate(otp: fetched_otp) if fetched_otp
|
101
|
+
puts "Failed to retrieve OTP from 1Password, falling back to manual entry."
|
102
|
+
end
|
103
|
+
|
104
|
+
# Fallback to manual OTP entry
|
105
|
+
fetched_manual_otp = prompt_for_manual_otp
|
106
|
+
return authenticate(otp: fetched_manual_otp) if fetched_manual_otp
|
107
|
+
|
108
|
+
raise "Could not retrieve OTP"
|
109
|
+
end
|
110
|
+
|
111
|
+
def onepassword_cli_available?
|
112
|
+
return @op_cli_available unless @op_cli_available.nil?
|
113
|
+
|
114
|
+
@op_cli_available = if ENV.key?("SDLS_FORCE_OP_CLI")
|
115
|
+
ENV["SDLS_FORCE_OP_CLI"] == "true"
|
116
|
+
else
|
117
|
+
system("which op > /dev/null 2>&1")
|
118
|
+
end
|
103
119
|
end
|
104
120
|
|
105
121
|
def fetch_otp_from_1password
|
@@ -113,5 +129,10 @@ module SDLS
|
|
113
129
|
nil
|
114
130
|
end
|
115
131
|
end
|
132
|
+
|
133
|
+
def prompt_for_manual_otp
|
134
|
+
prompt = TTY::Prompt.new
|
135
|
+
prompt.mask("Please enter your OTP code:")
|
136
|
+
end
|
116
137
|
end
|
117
138
|
end
|
data/lib/sdls/config.rb
CHANGED
@@ -1,31 +1,189 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "yaml"
|
4
|
+
require "open3"
|
5
|
+
require "tty-prompt"
|
4
6
|
|
5
7
|
module SDLS
|
6
|
-
|
8
|
+
# Configuration errors
|
9
|
+
class ConfigError < StandardError; end
|
10
|
+
|
11
|
+
class OnePasswordError < StandardError; end
|
12
|
+
|
13
|
+
# Core configuration keys that must be present
|
14
|
+
REQUIRED_KEYS = %i[host].freeze
|
15
|
+
|
7
16
|
Config = Data.define(:host, :username, :password, :op_item_name, :directories) do
|
8
|
-
|
9
|
-
|
17
|
+
class << self
|
18
|
+
def load(path, prompt: nil)
|
19
|
+
validate_file_exists!(path)
|
20
|
+
data = load_and_parse_yaml(path)
|
21
|
+
credentials = resolve_credentials(data, prompt: prompt)
|
10
22
|
|
11
|
-
|
12
|
-
|
13
|
-
|
23
|
+
new(**data.merge(credentials))
|
24
|
+
rescue Psych::SyntaxError => e
|
25
|
+
raise ConfigError, "Error parsing configuration file (#{path}): #{e.message}"
|
26
|
+
end
|
14
27
|
|
15
|
-
|
16
|
-
nil_keys = REQUIRED_KEYS.select { |k| data[k].nil? }
|
28
|
+
private
|
17
29
|
|
18
|
-
|
19
|
-
raise "Configuration file
|
30
|
+
def validate_file_exists!(path)
|
31
|
+
raise ConfigError, "Configuration file not found: #{path}" unless File.exist?(path)
|
20
32
|
end
|
21
33
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
34
|
+
def load_and_parse_yaml(path)
|
35
|
+
data = YAML.load_file(path)
|
36
|
+
data = {} unless data.is_a?(Hash)
|
37
|
+
data = symbolize_keys(data)
|
38
|
+
|
39
|
+
validate_required_keys!(data, path)
|
40
|
+
set_defaults(data)
|
41
|
+
end
|
42
|
+
|
43
|
+
def validate_required_keys!(data, path)
|
44
|
+
missing_keys = REQUIRED_KEYS - data.keys
|
45
|
+
nil_keys = REQUIRED_KEYS.select { |k| data[k].nil? || data[k].to_s.strip.empty? }
|
46
|
+
|
47
|
+
if missing_keys.any? || nil_keys.any?
|
48
|
+
invalid_keys = (missing_keys + nil_keys).uniq
|
49
|
+
raise ConfigError, "Configuration file (#{path}) is missing required keys or values: #{invalid_keys.join(", ")}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_defaults(data)
|
54
|
+
data[:op_item_name] ||= nil
|
55
|
+
data[:directories] ||= []
|
56
|
+
data
|
57
|
+
end
|
58
|
+
|
59
|
+
def symbolize_keys(hash)
|
60
|
+
hash.to_h { |k, v| [k.to_sym, v] }
|
61
|
+
end
|
62
|
+
|
63
|
+
def resolve_credentials(data, prompt: nil)
|
64
|
+
op_item_name = data[:op_item_name]
|
65
|
+
|
66
|
+
# Check if we have both username and password from config
|
67
|
+
has_config_username = data[:username] && !data[:username].to_s.strip.empty?
|
68
|
+
has_config_password = data[:password] && !data[:password].to_s.strip.empty?
|
69
|
+
|
70
|
+
# If we have both credentials from config, use them
|
71
|
+
if has_config_username && has_config_password
|
72
|
+
return {
|
73
|
+
username: data[:username],
|
74
|
+
password: data[:password]
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
# If 1Password item is specified and we're missing some credentials, try to fetch from there
|
79
|
+
if op_item_name && !op_item_name.to_s.strip.empty? && (!has_config_username || !has_config_password)
|
80
|
+
onepassword_credentials = fetch_credentials_from_1password(op_item_name)
|
81
|
+
if onepassword_credentials[:success]
|
82
|
+
final_username = has_config_username ? data[:username] : onepassword_credentials[:username]
|
83
|
+
final_password = has_config_password ? data[:password] : onepassword_credentials[:password]
|
84
|
+
|
85
|
+
# If we still don't have username or password after 1Password, prompt for them
|
86
|
+
final_username ||= prompt_for_credential("username", mask: false, prompt: prompt)
|
87
|
+
final_password ||= prompt_for_credential("password", mask: true, prompt: prompt)
|
88
|
+
|
89
|
+
return {
|
90
|
+
username: final_username,
|
91
|
+
password: final_password
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Fallback to config file and manual entry
|
97
|
+
{
|
98
|
+
username: resolve_username(data[:username], nil, prompt: prompt),
|
99
|
+
password: resolve_password(data[:password], nil, prompt: prompt)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def resolve_username(config_username, op_username, prompt: nil)
|
104
|
+
# Priority 1: 1Password username if available
|
105
|
+
return op_username if op_username && !op_username.strip.empty?
|
26
106
|
|
27
|
-
|
28
|
-
|
107
|
+
# Priority 2: Config file username if available
|
108
|
+
return config_username if config_username && !config_username.strip.empty?
|
109
|
+
|
110
|
+
# Priority 3: Manual entry
|
111
|
+
prompt_for_credential("username", mask: false, prompt: prompt)
|
112
|
+
end
|
113
|
+
|
114
|
+
def resolve_password(config_password, op_password, prompt: nil)
|
115
|
+
# Priority 1: 1Password password if available
|
116
|
+
return op_password if op_password && !op_password.strip.empty?
|
117
|
+
|
118
|
+
# Priority 2: Config file password if available
|
119
|
+
return config_password if config_password && !config_password.strip.empty?
|
120
|
+
|
121
|
+
# Priority 3: Manual entry
|
122
|
+
prompt_for_credential("password", mask: true, prompt: prompt)
|
123
|
+
end
|
124
|
+
|
125
|
+
def fetch_credentials_from_1password(op_item_name)
|
126
|
+
return {success: false} unless onepassword_cli_available?
|
127
|
+
|
128
|
+
puts "Fetching credentials from 1Password for item: #{op_item_name}..."
|
129
|
+
|
130
|
+
username = fetch_field_from_1password(op_item_name, "username")
|
131
|
+
password = fetch_field_from_1password(op_item_name, "password")
|
132
|
+
|
133
|
+
puts "1Password item '#{op_item_name}' retrieved successfully."
|
134
|
+
puts "Username: #{username.nil? ? "not found" : username}"
|
135
|
+
|
136
|
+
if username || password
|
137
|
+
success_msg = []
|
138
|
+
success_msg << "username" if username
|
139
|
+
success_msg << "password" if password
|
140
|
+
puts "Successfully retrieved #{success_msg.join(" and ")} from 1Password"
|
141
|
+
|
142
|
+
{success: true, username: username, password: password}
|
143
|
+
else
|
144
|
+
puts "No credentials found in 1Password item"
|
145
|
+
{success: false}
|
146
|
+
end
|
147
|
+
rescue OnePasswordError => e
|
148
|
+
puts "1Password error: #{e.message}"
|
149
|
+
{success: false}
|
150
|
+
rescue => e
|
151
|
+
puts "Unexpected error fetching credentials from 1Password: #{e.message}"
|
152
|
+
{success: false}
|
153
|
+
end
|
154
|
+
|
155
|
+
def fetch_field_from_1password(op_item_name, field)
|
156
|
+
stdout, _, status = Open3.capture3(
|
157
|
+
"op", "item", "get", op_item_name, "--fields", field, "--reveal"
|
158
|
+
)
|
159
|
+
|
160
|
+
if status.success? && !stdout.strip.empty?
|
161
|
+
stdout.strip
|
162
|
+
end
|
163
|
+
rescue => e
|
164
|
+
raise OnePasswordError, "Failed to retrieve #{field} from 1Password: #{e.message}"
|
165
|
+
end
|
166
|
+
|
167
|
+
def onepassword_cli_available?
|
168
|
+
return @op_cli_available unless @op_cli_available.nil?
|
169
|
+
|
170
|
+
@op_cli_available = if ENV.key?("SDLS_FORCE_OP_CLI")
|
171
|
+
ENV["SDLS_FORCE_OP_CLI"] == "true"
|
172
|
+
else
|
173
|
+
system("which op > /dev/null 2>&1")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def prompt_for_credential(type, mask: false, prompt: nil)
|
178
|
+
puts "No #{type} available, please enter manually:"
|
179
|
+
prompt ||= TTY::Prompt.new
|
180
|
+
|
181
|
+
if mask
|
182
|
+
prompt.mask("Please enter your #{type}:")
|
183
|
+
else
|
184
|
+
prompt.ask("Please enter your #{type}:")
|
185
|
+
end
|
186
|
+
end
|
29
187
|
end
|
30
188
|
end
|
31
189
|
end
|
data/lib/sdls/version.rb
CHANGED
metadata
CHANGED
@@ -1,16 +1,58 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sdls
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Camillo Visini
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
-
dependencies:
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: thor
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.3'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.3'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: tty-prompt
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.23'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.23'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: clipboard
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
12
54
|
description: A command-line interface for managing downloads on Synology Download
|
13
|
-
Station
|
55
|
+
Station.
|
14
56
|
executables:
|
15
57
|
- sdls
|
16
58
|
extensions: []
|