filedepot 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2297095fcd44cd4bc54bf159dba4db6b67eb01097b81d7b568fbdd09b508394
4
- data.tar.gz: 5057493bcdc4ad7e83dbd6f6b1469b38b9317003674ecfd291c3c2c3c1e6b243
3
+ metadata.gz: d3adae8af29b5bf23fca9832843d431f9ecac1a3a185309185858109de26c9ea
4
+ data.tar.gz: 180afd3e6ad41a51a63c3ddd8ecfa5920519c9135180c74ea1cc10de631dd629
5
5
  SHA512:
6
- metadata.gz: 0f88deafeeed98b2b9ddcb4b47179b9da164c583f7e1b8690ac92976fd551e475ae73511b66eef239c59e735a31d878c5c074dd2802543503bd8d0dfc1a5b64a
7
- data.tar.gz: b8b7726ae00b0c3fd64faf4bf97112f6ff8b3183c5c9ea82f8f6a67b95bbdfb51bc776594dd2f5e181e439b3a6fc422c82a6b90dbded855cb2eedd77fd6700b1
6
+ metadata.gz: 2324d4048ddc0d01ad140393850b36c4615cf6104d60251d868563171499b6f5505f761ed1b225a0aa9851fcfadc7b9c6aed1bc9b6ddb068f51d4b9eee6c4d40
7
+ data.tar.gz: aea6ac1df0926cff12f09f9547126cb6c449343e62c91f40c1615397396c80608c531c4c2a02faf957f885e18357c92982abf9510f8e7a22c8e3589a3d41627c
data/lib/filedepot/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require "thor"
4
5
 
5
6
  module Filedepot
@@ -15,16 +16,24 @@ module Filedepot
15
16
  HELP
16
17
  push: <<~HELP,
17
18
  Usage:
18
- filedepot push HANDLE
19
+ filedepot push HANDLE FILE
19
20
 
20
- Send a file with a specific handle to the current storage.
21
+ Send a file to the current storage with a specific handle.
22
+ Example: filedepot push test test.txt
21
23
  HELP
22
24
  pull: <<~HELP,
23
25
  Usage:
24
- filedepot pull HANDLE [VERSION]
25
-
26
- Get the latest version of file with a specific handle from the current storage.
27
- VERSION is optional; if omitted, retrieves the latest version.
26
+ filedepot pull HANDLE [--path PATH] [--version VERSION]
27
+
28
+ Get a file from storage. By default gets the latest version and saves to current directory.
29
+ Options:
30
+ --path PATH Save to this local path (e.g. ./test/file.txt)
31
+ --version N Get specific version (default: latest)
32
+ Examples:
33
+ filedepot pull test
34
+ filedepot pull test --path ./test/file.txt
35
+ filedepot pull test --version 2
36
+ filedepot pull test --version 2 --path ./test/file.txt
28
37
  HELP
29
38
  versions: <<~HELP,
30
39
  Usage:
@@ -49,23 +58,53 @@ module Filedepot
49
58
  exec(editor, config_path)
50
59
  end
51
60
 
52
- desc "push HANDLE", "Send a file with a specific handle to the current storage"
53
- def push(handle = nil)
54
- if handle.nil?
55
- puts COMMAND_HELP[:push]
61
+ desc "push HANDLE FILE", "Send a file to the current storage with a specific handle"
62
+ def push(handle, file_path)
63
+ source = Config.current_source
64
+ if source.nil?
65
+ puts "Error: No storage source configured. Run 'filedepot config' to set up."
56
66
  return
57
67
  end
58
- puts "push: would send file with handle '#{handle}' to current storage (not implemented)"
68
+
69
+ storage = Storage::Base.for(source)
70
+ storage.push(handle, file_path)
71
+ puts "Pushed #{file_path} as #{handle} (version #{storage.current_version(handle)})"
59
72
  end
60
73
 
61
- desc "pull HANDLE [VERSION]", "Get the latest version of file with a specific handle from the current storage"
62
- def pull(handle = nil, version = nil)
74
+ desc "pull HANDLE", "Get file from storage"
75
+ method_option :path, type: :string, desc: "Local path to save the file"
76
+ method_option :version, type: :string, desc: "Version number to pull (default: latest)"
77
+ def pull(handle = nil)
63
78
  if handle.nil?
64
79
  puts COMMAND_HELP[:pull]
65
80
  return
66
81
  end
67
- version_str = version ? " version #{version}" : " latest version"
68
- puts "pull: would get file '#{handle}'#{version_str} from current storage (not implemented)"
82
+
83
+ source = Config.current_source
84
+ if source.nil?
85
+ puts "Error: No storage source configured. Run 'filedepot config' to set up."
86
+ return
87
+ end
88
+
89
+ local_path = options[:path]
90
+ version = (options[:version].nil? || options[:version].empty?) ? nil : options[:version].to_i
91
+
92
+ storage = Storage::Base.for(source)
93
+ info = storage.pull_info(handle, version, local_path)
94
+ target_path = info[:target_path]
95
+
96
+ parent_dir = File.dirname(target_path)
97
+ unless parent_dir == "." || File.directory?(parent_dir)
98
+ return unless confirm?("create local directory #{parent_dir}, y/n?")
99
+ FileUtils.mkdir_p(parent_dir)
100
+ end
101
+
102
+ if File.exist?(target_path)
103
+ return unless confirm?("overwrite local #{target_path}, y/n?")
104
+ end
105
+
106
+ storage.pull(handle, version, target_path)
107
+ puts "Pulled #{handle} (version #{info[:version_num]}) to #{target_path}"
69
108
  end
70
109
 
71
110
  desc "versions HANDLE", "List all versions of a handle (each version has an integer from 1 to n)"
@@ -74,7 +113,26 @@ module Filedepot
74
113
  puts COMMAND_HELP[:versions]
75
114
  return
76
115
  end
77
- puts "versions: would list all versions of handle '#{handle}' (not implemented)"
116
+
117
+ source = Config.current_source
118
+ if source.nil?
119
+ puts "Error: No storage source configured. Run 'filedepot config' to set up."
120
+ return
121
+ end
122
+
123
+ storage = Storage::Base.for(source)
124
+ versions_list = storage.versions(handle)
125
+ if versions_list.empty?
126
+ puts "No versions found for handle: #{handle}"
127
+ else
128
+ max_display = 10
129
+ to_show = versions_list.first(max_display)
130
+ to_show.each { |v, date_str| puts date_str.empty? ? v.to_s : "#{v} - #{date_str}" }
131
+ if versions_list.size > max_display
132
+ remaining = versions_list.size - max_display
133
+ puts "... and #{remaining} other ones for a total of #{versions_list.size} versions"
134
+ end
135
+ end
78
136
  end
79
137
 
80
138
  desc "delete HANDLE [VERSION]", "After confirmation, delete all versions of a file; or only a specific version if specified"
@@ -105,8 +163,8 @@ module Filedepot
105
163
  puts ""
106
164
  puts "Available commands:"
107
165
  puts " filedepot config Open config file using $EDITOR"
108
- puts " filedepot push HANDLE Send file to current storage"
109
- puts " filedepot pull HANDLE [VER] Get file from storage (version optional)"
166
+ puts " filedepot push HANDLE FILE Send file to current storage"
167
+ puts " filedepot pull HANDLE [--path PATH] [--version N] Get file from storage"
110
168
  puts " filedepot versions HANDLE List all versions of a handle"
111
169
  puts " filedepot delete HANDLE [VER] Delete file(s) after confirmation"
112
170
  puts ""
@@ -116,5 +174,16 @@ module Filedepot
116
174
  def self.exit_on_failure?
117
175
  true
118
176
  end
177
+
178
+ private
179
+
180
+ def confirm?(prompt)
181
+ print "#{prompt} "
182
+ input = $stdin.gets&.strip&.downcase
183
+ input == "y" || input == "yes"
184
+ rescue Interrupt
185
+ puts
186
+ false
187
+ end
119
188
  end
120
189
  end
@@ -36,7 +36,10 @@ module Filedepot
36
36
  config = load
37
37
  default = config["default_source"]
38
38
  sources = config["sources"] || []
39
- sources.find { |s| s["name"] == default }
39
+ return nil if sources.empty?
40
+
41
+ source = sources.find { |s| (s["name"] || s[:name]) == default }
42
+ source || sources.first
40
43
  end
41
44
  end
42
45
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Filedepot
4
+ module Storage
5
+ class Base
6
+ def self.for(source)
7
+ if source["ssh"]
8
+ SshStorage.new(source)
9
+ else
10
+ raise ArgumentError, "Unknown storage type for source: #{source["name"]}"
11
+ end
12
+ end
13
+
14
+ def initialize(source)
15
+ @source = source
16
+ end
17
+
18
+ def current_version(handle)
19
+ path = current_version_path(handle)
20
+ path.nil? || path.to_s.empty? ? 0 : File.basename(path).to_i
21
+ end
22
+
23
+ def next_version(handle)
24
+ current_version(handle) + 1
25
+ end
26
+
27
+ def next_version_path(handle)
28
+ File.join(remote_handle_path(handle), next_version(handle).to_s)
29
+ end
30
+
31
+ def current_version_path(handle)
32
+ raise "not implemented"
33
+ end
34
+
35
+ def test
36
+ raise "not implemented"
37
+ end
38
+
39
+ def ls
40
+ raise "not implemented"
41
+ end
42
+
43
+ def push(handle, local_path)
44
+ raise "not implemented"
45
+ end
46
+
47
+ def pull(handle, version = nil, local_path = nil)
48
+ raise "not implemented"
49
+ end
50
+
51
+ def versions(handle)
52
+ raise "not implemented"
53
+ end
54
+
55
+ def delete(handle, version = nil)
56
+ raise "not implemented"
57
+ end
58
+
59
+ protected
60
+
61
+ def remote_base_path
62
+ @source["base_path"] || "/tmp/filedepot"
63
+ end
64
+
65
+ def remote_handle_path(handle)
66
+ safe_handle = handle.to_s.gsub(%r{[^\w\-./]}, "_")
67
+ File.join(remote_base_path, safe_handle)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+ require "net/ssh"
5
+ require "net/scp"
6
+
7
+ module Filedepot
8
+ module Storage
9
+ class SshStorage < Base
10
+ def test
11
+ ssh_session do |ssh|
12
+ result = ssh.exec!("echo ok")
13
+ raise "Connection failed" unless result&.include?("ok")
14
+ end
15
+ end
16
+
17
+ def ls
18
+ handles = []
19
+ ssh_session do |ssh|
20
+ result = ssh.exec!("ls -1 #{shell_escape(remote_base_path)} 2>/dev/null || true")
21
+ return [] if result.nil? || result.strip.empty?
22
+
23
+ handles = result.strip.split("\n").select { |line| !line.empty? }
24
+ end
25
+ handles
26
+ end
27
+
28
+ def push(handle, local_path)
29
+ path = File.expand_path(local_path)
30
+ raise "File not found: #{local_path}" unless File.file?(path)
31
+
32
+ push_path = next_version_path(handle)
33
+
34
+ ssh_session do |ssh|
35
+ ssh.exec!("mkdir -p #{shell_escape(push_path)}")
36
+ # Upload to directory - SCP places file with original name inside
37
+ ssh.scp.upload!(path, push_path)
38
+ end
39
+ end
40
+
41
+ def current_version_path(handle)
42
+ ssh_session do |ssh|
43
+ handle_dir = remote_handle_path(handle)
44
+ ssh.exec!("mkdir -p #{shell_escape(handle_dir)}")
45
+
46
+ result = ssh.exec!("ls -1 #{shell_escape(handle_dir)} 2>/dev/null || true")
47
+ versions = parse_versions(result)
48
+
49
+ if versions.empty?
50
+ nil
51
+ else
52
+ File.join(handle_dir, versions.max.to_s)
53
+ end
54
+ end
55
+ end
56
+
57
+ def pull(handle, version = nil, local_path = nil)
58
+ ssh_session do |ssh|
59
+ versions_list = versions_for(ssh, handle)
60
+ raise "No versions found for handle: #{handle}" if versions_list.empty?
61
+
62
+ version_num = version ? version.to_i : versions_list.max
63
+ raise "Version #{version} not found" unless versions_list.include?(version_num)
64
+
65
+ version_dir = File.join(remote_handle_path(handle), version_num.to_s)
66
+ remote_file = first_file_in_dir(ssh, version_dir)
67
+ raise "No file found in version #{version_num}" if remote_file.nil?
68
+
69
+ remote_filename = File.basename(remote_file)
70
+ target_path = resolve_local_path(local_path, remote_filename)
71
+
72
+ ssh.scp.download!(remote_file, target_path)
73
+ target_path
74
+ end
75
+ end
76
+
77
+ def pull_info(handle, version = nil, local_path = nil)
78
+ ssh_session do |ssh|
79
+ versions_list = versions_for(ssh, handle)
80
+ raise "No versions found for handle: #{handle}" if versions_list.empty?
81
+
82
+ version_num = version ? version.to_i : versions_list.max
83
+ raise "Version #{version} not found" unless versions_list.include?(version_num)
84
+
85
+ version_dir = File.join(remote_handle_path(handle), version_num.to_s)
86
+ remote_file = first_file_in_dir(ssh, version_dir)
87
+ raise "No file found in version #{version_num}" if remote_file.nil?
88
+
89
+ remote_filename = File.basename(remote_file)
90
+ target_path = resolve_local_path(local_path, remote_filename)
91
+
92
+ { remote_filename: remote_filename, version_num: version_num, target_path: target_path }
93
+ end
94
+ end
95
+
96
+ def versions(handle)
97
+ ssh_session do |ssh|
98
+ versions_list = versions_for(ssh, handle).sort.reverse
99
+ versions_list.map do |v|
100
+ version_dir = File.join(remote_handle_path(handle), v.to_s)
101
+ epoch = stat_mtime(ssh, version_dir)
102
+ date_str = epoch ? Time.at(epoch).to_s : ""
103
+ [v, date_str]
104
+ end
105
+ end
106
+ end
107
+
108
+ def delete(handle, version = nil)
109
+ ssh_session do |ssh|
110
+ handle_dir = remote_handle_path(handle)
111
+
112
+ if version
113
+ version_dir = File.join(handle_dir, version.to_s)
114
+ ssh.exec!("rm -rf #{shell_escape(version_dir)}")
115
+ versions_remaining = versions_for(ssh, handle)
116
+ ssh.exec!("rmdir #{shell_escape(handle_dir)} 2>/dev/null || true") if versions_remaining.empty?
117
+ else
118
+ ssh.exec!("rm -rf #{shell_escape(handle_dir)}")
119
+ end
120
+ end
121
+ end
122
+
123
+ private
124
+
125
+ def shell_escape(path)
126
+ Shellwords.shellescape(path.to_s)
127
+ end
128
+
129
+ def ssh_session
130
+ host = @source["host"] || "localhost"
131
+ user = @source["username"].to_s.strip
132
+ user = ENV["USER"] if user.empty?
133
+
134
+ Net::SSH.start(host, user) do |ssh|
135
+ yield ssh
136
+ end
137
+ end
138
+
139
+ def versions_for(ssh, handle)
140
+ remote_dir = remote_handle_path(handle)
141
+ result = ssh.exec!("ls -1 #{shell_escape(remote_dir)} 2>/dev/null || true")
142
+ parse_versions(result)
143
+ end
144
+
145
+ def parse_versions(ls_result)
146
+ return [] if ls_result.nil? || ls_result.strip.empty?
147
+
148
+ ls_result.strip.split("\n").map(&:to_i).select { |v| v.positive? }
149
+ end
150
+
151
+ def first_file_in_dir(ssh, dir)
152
+ result = ssh.exec!("ls -1 #{shell_escape(dir)} 2>/dev/null || true")
153
+ return nil if result.nil? || result.strip.empty?
154
+
155
+ first = result.strip.split("\n").first
156
+ first ? File.join(dir, first) : nil
157
+ end
158
+
159
+ def stat_mtime(ssh, path)
160
+ # Linux: stat -c %Y; macOS: stat -f %m
161
+ result = ssh.exec!("stat -c %Y #{shell_escape(path)} 2>/dev/null || stat -f %m #{shell_escape(path)} 2>/dev/null")
162
+ result&.strip&.to_i
163
+ end
164
+
165
+ def resolve_local_path(local_path_param, remote_filename)
166
+ if local_path_param.nil? || local_path_param.empty?
167
+ File.join(Dir.pwd, remote_filename)
168
+ elsif local_path_param.end_with?("/") || (File.exist?(local_path_param) && File.directory?(local_path_param))
169
+ File.join(File.expand_path(local_path_param), remote_filename)
170
+ else
171
+ File.expand_path(local_path_param)
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Filedepot
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/filedepot.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "filedepot/version"
4
4
  require "filedepot/config"
5
+ require "filedepot/storage/base"
6
+ require "filedepot/storage_ssh"
5
7
  require "filedepot/cli"
6
8
 
7
9
  module Filedepot
metadata CHANGED
@@ -1,15 +1,42 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: filedepot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Filedepot
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-02-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: net-scp
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '4.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '4.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: net-ssh
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '7.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
13
40
  - !ruby/object:Gem::Dependency
14
41
  name: thor
15
42
  requirement: !ruby/object:Gem::Requirement
@@ -37,6 +64,8 @@ files:
37
64
  - lib/filedepot.rb
38
65
  - lib/filedepot/cli.rb
39
66
  - lib/filedepot/config.rb
67
+ - lib/filedepot/storage/base.rb
68
+ - lib/filedepot/storage_ssh.rb
40
69
  - lib/filedepot/version.rb
41
70
  homepage: https://github.com/filedepot/filedepot
42
71
  licenses:
@@ -44,7 +73,6 @@ licenses:
44
73
  metadata:
45
74
  homepage_uri: https://github.com/filedepot/filedepot
46
75
  source_code_uri: https://github.com/filedepot/filedepot
47
- post_install_message:
48
76
  rdoc_options: []
49
77
  require_paths:
50
78
  - lib
@@ -59,8 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
87
  - !ruby/object:Gem::Version
60
88
  version: '0'
61
89
  requirements: []
62
- rubygems_version: 3.5.22
63
- signing_key:
90
+ rubygems_version: 4.0.6
64
91
  specification_version: 4
65
92
  summary: Sync files on remote storage
66
93
  test_files: []