remotesync 1.0.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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/bin/rrinit +184 -0
  3. data/bin/rrpull +75 -0
  4. data/bin/rrpush +35 -0
  5. data/lib/commons.rb +284 -0
  6. metadata +70 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13cc5deafe2ae0fa3348ac64b6212feef2b8df773b4005a49d134652e86a99d4
4
+ data.tar.gz: 9cb6df03332a34df4320776273ec54b4327b1f49cae294e599ef9decc24063a3
5
+ SHA512:
6
+ metadata.gz: 892ac784c4fd58041b470056d862d1b0c8e57a31f889d4857cbedd27ad2af05b766dc9d3ab39b0de6798a174f23d683f7a90ec05c9271edc86ffc4ce0047c3d7
7
+ data.tar.gz: f65bf5a6b664b96078fda80e56b0cab90f9f61acc28331a1c235e015e62f66d7984de0716df9a1f551e36fc92d1e59202f33315226baafb9e699a89b562132d6
data/bin/rrinit ADDED
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "../lib/commons"
4
+
5
+ args = CmdParser.setup(INIT_COMMAND, :hostname, :path) do |opts|
6
+ CmdParser.autoset_param "-pPORT", "--port=PORT", "Port of the remote host", :port, [:propagate, :delete, :unlock, :status]
7
+ CmdParser.autoset_param "-uUSER", "--user=USER", "User of the remote host", :user, [:propagate, :delete, :unlock, :status]
8
+ CmdParser.autoset_param "-nNETNS", "--netns=NETNS", "Network namespace", :netns, [:propagate, :delete, :unlock, :status]
9
+ CmdParser.autoset_param "-oOWNER", "--owner=OWNER", "Ownership of the files (defaults to the current user)", :owner, [:propagate, :delete, :unlock, :status]
10
+
11
+ CmdParser.autoset_flag "-r", "--recursive", "Ownership of the files (defaults to the current user)", :recursive, [:propagate]
12
+ CmdParser.autoset_flag "-f", "--force", "Force the overwrite of existing data", :force
13
+ CmdParser.autoset_flag "-R", "--pull-only", "Allows only the pull from the remote", :pullonly, [:pushonly]
14
+ CmdParser.autoset_flag "-W", "--push-only", "Allows only the push to the remote", :pushonly, [:pullonly]
15
+
16
+ CmdParser.autoset_flag "-D", "--delete", "Delete all the sync files (asks for confirmation)", :delete, [:propagate, :unlock, :status]
17
+ CmdParser.autoset_flag "-p", "--propagate", "Propagate the root file to all the sub-directories", :propagate, [:delete, :unlock, :status]
18
+ CmdParser.autoset_flag "-u", "--unlock", "Unlock all the remote information files", :unlock, [:delete, :propagate, :status]
19
+ CmdParser.autoset_flag "-s", "--status", "Prints the status of the folder", :status, [:delete, :propagate, :unlock]
20
+ end
21
+
22
+ if CmdParser.opt :delete
23
+ unless FileTest.exists? RemoteInfo.filename(".")
24
+ puts "This folder is not initialized yet. " + "Deletion aborted.".red
25
+ exit
26
+ end
27
+
28
+ sub = CmdParser.opt(:recursive) ? " and its subdirectories" : ""
29
+ print "Are you sure you want to delete all the sync information in this directory#{sub}? [y/N] "
30
+ confirm = gets.chomp.downcase
31
+ if confirm != "y"
32
+ puts "Aborted.".red
33
+ exit
34
+ end
35
+
36
+ print "Deleting all the remote files... "
37
+ puts "" if CmdParser.opt :verbose
38
+ FS.each_folder(".", CmdParser.opt(:recursive)) do |current|
39
+ vputs "Deleting remote information file for #{current}"
40
+ OS.remove_file RemoteInfo.filename(current)
41
+ end
42
+
43
+ puts "Done!".green
44
+ exit
45
+
46
+ elsif CmdParser.opt :propagate
47
+ print "Propagating the remote information to all the subfolders... "
48
+ unless FileTest.exists? RemoteInfo.filename(".")
49
+ puts "\n" + "Error".red + ": Initialize this directory first. Aborting propagation."
50
+ exit
51
+ end
52
+
53
+ rfile = RemoteInfo.load
54
+
55
+ CmdParser.set :port, rfile.port
56
+ CmdParser.set :user, rfile.user
57
+ CmdParser.set :netns, rfile.netns
58
+ CmdParser.set :owner, rfile.owner
59
+ CmdParser.set :recursive, true
60
+ args[:hostname] = rfile.host
61
+ args[:path] = rfile.path
62
+
63
+ vputs "\nPropagation information:"
64
+ vputs "\tHost: #{rfile.host}"
65
+ vputs "\tPath: #{rfile.path}"
66
+ vputs "\tPort: #{rfile.port}" if rfile.port
67
+ vputs "\tUser: #{rfile.user}" if rfile.user
68
+ vputs "\tNetwork namespace: #{rfile.netns}" if rfile.netns
69
+ vputs "\tOwner: #{rfile.owner}" if rfile.owner
70
+ #Does propagation through the normal procedure
71
+
72
+ elsif CmdParser.opt :unlock
73
+ print "Unlocking all the remote files... "
74
+ RemoteInfo.unlock_all current
75
+ puts "Done!".green
76
+ exit
77
+
78
+ elsif CmdParser.opt :status
79
+ unless FileTest.exists? RemoteInfo.filename(".")
80
+ puts "This folder is not initialized yet."
81
+ exit
82
+ end
83
+
84
+ rinfo = RemoteInfo.load "."
85
+ userstring = rinfo.user ? rinfo.user.to_s + "@" : ""
86
+ portstring = rinfo.port ? ":"+rinfo.port.to_s : ""
87
+
88
+ mode = []
89
+ if rinfo.can_push?
90
+ mode.push "Push".green
91
+ else
92
+ mode.push "Push".red
93
+ end
94
+
95
+ if rinfo.can_pull?
96
+ mode.push "Pull".green
97
+ else
98
+ mode.push "Pull".red
99
+ end
100
+ modestring = mode.join("|")
101
+
102
+ puts "Root remote information:"
103
+ puts "\tHost: #{userstring}#{rinfo.host}#{portstring}"
104
+ puts "\tPath: #{rinfo.path}"
105
+ puts "\tNetwork namespace: #{rinfo.netns}" if rinfo.netns
106
+ puts "\tOwner: #{rinfo.owner}" if rinfo.owner
107
+ puts "\tMode: #{modestring}"
108
+
109
+ puts "Local folders:"
110
+ ignore = []
111
+ FS.each_folder(".", true) do |current|
112
+ next if current == "."
113
+
114
+ skip = false
115
+ ignore.each do |ig|
116
+ skip = true if current.start_with? ig
117
+ end
118
+ next if skip
119
+
120
+ valid = File.join(rinfo.path, current)
121
+
122
+ status = ""
123
+ if FileTest.exists? RemoteInfo.filename(current)
124
+ cinfo = RemoteInfo.load current
125
+
126
+ if cinfo.path != valid || cinfo.host != rinfo.host
127
+ status = "DIFFERENT REMOTE".yellow + "(" + ScpUtils.remote(nil, cinfo) + ")"
128
+ ignore.push current
129
+ else
130
+ status = "OK".green
131
+ end
132
+ else
133
+ status = "LOCAL ONLY".red
134
+ ignore.push current
135
+ end
136
+ puts "\t- #{current}: #{status}"
137
+ end
138
+ exit
139
+
140
+ end
141
+
142
+
143
+ err = false
144
+ unless args[:hostname]
145
+ puts "Error".red + ": You have to specify the remote hostname."
146
+ err = true
147
+ end
148
+
149
+ unless args[:path]
150
+ puts "Error".red + ": You have to specify the remote path."
151
+ err = true
152
+ end
153
+
154
+ if err
155
+ puts "Try #{INIT_COMMAND} -h to check how to use this command"
156
+ exit
157
+ end
158
+
159
+ unless CmdParser.opt :propagate
160
+ sub = CmdParser.opt(:recursive) ? " and its subdirectories" : ""
161
+ print "Initializing the directory#{sub}... "
162
+ end
163
+
164
+ FS.each_folder(".", CmdParser.opt(:recursive)) do |current|
165
+ if CmdParser.opt(:force) || !FileTest.exists?(RemoteInfo.filename(current))
166
+ rfile = RemoteInfo.new
167
+ rfile.port = CmdParser.opt :port
168
+ rfile.user = CmdParser.opt :user
169
+ rfile.netns = CmdParser.opt :netns
170
+ rfile.owner = CmdParser.opt :owner
171
+ rfile.operations = "w" if CmdParser.opt :pushonly
172
+ rfile.operations = "r" if CmdParser.opt :pullonly
173
+
174
+ rfile.host = args[:hostname]
175
+ rfile.path = (current != "." ? File.join(args[:path], current) : args[:path])
176
+
177
+ vputs "Writing remote information for #{current}..."
178
+ rfile.save_info current
179
+ else
180
+ vputs "Skipping #{current} (already exists)..."
181
+ end
182
+ end
183
+
184
+ puts "Done!".green
data/bin/rrpull ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "../lib/commons"
4
+
5
+ args = CmdParser.setup(PULL_COMMAND) do |opts|
6
+ CmdParser.autoset_param "-fFILE", "--file=FILE", "File to push", :file, [:directory]
7
+ CmdParser.autoset_param "-dDIR", "--directory=DIR", "Directory to push", :directory, [:file]
8
+
9
+ CmdParser.autoset_param "-oOWNER", "--owner=OWNER", "Ownership of the files (defaults to the current user)", :owner
10
+ end
11
+
12
+ unless FileTest.exists? RemoteInfo.filename(".")
13
+ puts "Error".red + ": please, use #{INIT_COMMAND} before #{PULL_COMMAND}"
14
+ exit
15
+ end
16
+
17
+ remote_info = RemoteInfo.load
18
+
19
+ unless remote_info.can_pull?
20
+ puts "Error".red + ": pull forbidden (push-only directory). Aborted."
21
+ exit
22
+ end
23
+
24
+ isdir = !CmdParser.opt(:file)
25
+
26
+ OS.run "mkdir -p \"#{CmdParser.opt(:directory)}\"" if CmdParser.opt(:directory)
27
+
28
+ #Lock all the remote information files
29
+ RemoteInfo.lock_all(CmdParser.opt(:directory) || ".", isdir) do
30
+ vputs "Locked all the remote information files in the path"
31
+ local = ScpUtils.local CmdParser.opt(:file), CmdParser.opt(:directory)
32
+ remote = ScpUtils.remote local, remote_info
33
+ remote = File.join(remote, ".")
34
+
35
+ command = ScpUtils.scp remote, local, isdir, remote_info.port
36
+
37
+ new_owner = CmdParser.opt(:owner) || remote_info.owner || OS.username
38
+ result = nil
39
+ if remote_info.netns
40
+ puts "Requiring root privileges to run in network namespace..."
41
+ result = OS.run "sudo ip netns exec #{remote_info.netns} #{command}"
42
+ new_owner = OS.username unless new_owner
43
+ else
44
+ result = OS.run command
45
+ end
46
+
47
+ unless result
48
+ puts "An error occurred while executing the copy command. Exiting"
49
+ RemoteInfo.unlock_all
50
+ exit
51
+ end
52
+
53
+ result[1].split("\n").each do |errline|
54
+ if errline.downcase.include?("permission denied") && !errline.downcase.include?(RemoteInfo::FILENAME)
55
+ puts "Error:" + errline
56
+ end
57
+ end
58
+
59
+ if new_owner
60
+ puts "Requiring root privileges to change ownership..."
61
+ OS.run "sudo chown #{new_owner} \"#{local}\" #{isdir ? "-R" : ""}"
62
+ end
63
+ vputs "Unlocked all the remote information files in the path"
64
+ end
65
+
66
+ rinit_script = File.join File.expand_path(File.dirname(__FILE__)), INIT_COMMAND
67
+ #Propagates the creation of remote file to all the new subfolders
68
+ vputs "Calling #{INIT_COMMAND} to propagate the information..."
69
+ result = OS.run "ruby \"#{rinit_script}\" -p"
70
+ puts result[0]
71
+ if result[1] != ""
72
+ puts "Error".red + ":" + result[1]
73
+ end
74
+
75
+ puts "The local version is up to date.".green
data/bin/rrpush ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require_relative "../lib/commons"
4
+
5
+ args = CmdParser.setup(PUSH_COMMAND) do |opts|
6
+ CmdParser.autoset_param "-fFILE", "--file=FILE", "File to push", :file, [:directory]
7
+ CmdParser.autoset_param "-dDIR", "--directory=DIR", "Directory to push", :directory, [:file]
8
+ end
9
+
10
+ unless FileTest.exists? RemoteInfo.filename(".")
11
+ puts "Error".red + ": please, use #{INIT_COMMAND} before #{PUSH_COMMAND}"
12
+ exit
13
+ end
14
+
15
+ remote_info = RemoteInfo.load
16
+
17
+ unless remote_info.can_push?
18
+ puts "Error".red + ": push forbidden (pull-only directory). Aborted."
19
+ exit
20
+ end
21
+
22
+ isdir = !CmdParser.opt(:file)
23
+
24
+ local = ScpUtils.local CmdParser.opt(:file), CmdParser.opt(:directory)
25
+ remote = ScpUtils.remote local, remote_info
26
+
27
+ command = ScpUtils.scp local, remote, isdir, remote_info.port
28
+ if remote_info.netns
29
+ puts "Requiring root privileges to run in network namespace..."
30
+ OS.run "sudo ip netns exec #{remote_info.netns} #{command}"
31
+ else
32
+ OS.run command
33
+ end
34
+
35
+ puts "The remote version is up to date.".green
data/lib/commons.rb ADDED
@@ -0,0 +1,284 @@
1
+ require "optparse"
2
+ require "open3"
3
+ require "colorize"
4
+
5
+ INIT_COMMAND = "rrinit"
6
+ PUSH_COMMAND = "rrpush"
7
+ PULL_COMMAND = "rrpull"
8
+
9
+ class RemoteInfo
10
+ FILENAME = ".reminfo"
11
+
12
+ attr_accessor :host
13
+ attr_accessor :port
14
+ attr_accessor :user
15
+ attr_accessor :path
16
+ attr_accessor :netns
17
+ attr_accessor :owner
18
+ attr_accessor :operations
19
+
20
+ def initialize
21
+ @netns = nil
22
+ @owner = nil
23
+
24
+ @port = nil
25
+ @user = nil
26
+
27
+ @operations = "rw"
28
+ end
29
+
30
+ def self.load(folder=".")
31
+ rinfo = RemoteInfo.new
32
+ rinfo.read_info(folder)
33
+ return rinfo
34
+ end
35
+
36
+ def self.filename(folder)
37
+ return File.join(folder, FILENAME)
38
+ end
39
+
40
+ def can_pull?
41
+ return @operations.include? "r"
42
+ end
43
+
44
+ def can_push?
45
+ return @operations.include? "w"
46
+ end
47
+
48
+ def read_info(folder)
49
+ info = File.read(File.join(folder, FILENAME))
50
+
51
+ rows = info.split "\n"
52
+ rows.each do |r|
53
+ key, value = *r.split("\t")
54
+
55
+ key = key.strip
56
+ value = value.strip
57
+
58
+ case key
59
+ when "host"
60
+ @host = value
61
+ when "port"
62
+ @port = value
63
+ when "user"
64
+ @user = value
65
+ when "path"
66
+ @path = value
67
+ when "netns"
68
+ @netns = value
69
+ when "owner"
70
+ @owner = value
71
+ when "operations"
72
+ @operations = value
73
+ end
74
+ end
75
+ end
76
+
77
+ def save_info(to=".")
78
+ content = ""
79
+ content += "host\t#@host\n"
80
+ content += "port\t#@port\n" if @port
81
+ content += "user\t#@user\n" if @user
82
+ content += "path\t#@path\n"
83
+ content += "netns\t#@netns\n" if @netns
84
+ content += "owner\t#@owner\n" if @owner
85
+ content += "operations\t#@operations\n"
86
+
87
+ dest = to ? File.join(to, FILENAME) : FILENAME
88
+
89
+ File.write dest, content
90
+ end
91
+
92
+ def self.unlock_all(local)
93
+ self.lock_all(local, true) {}
94
+ end
95
+
96
+ def self.lock_all(local, prevent_write=true)
97
+ if (prevent_write)
98
+ FS.each_remote_info(local) do |rinfo|
99
+ OS.run "chmod -w \"#{rinfo}\""
100
+ end
101
+ end
102
+
103
+ yield
104
+
105
+ if (prevent_write)
106
+ FS.each_remote_info(local) do |rinfo|
107
+ OS.run "chmod +w \"#{rinfo}\""
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ class FS
114
+ def self.each_folder(dir, recursive=true)
115
+ queue = ["."]
116
+ visited = []
117
+ while queue.size > 0
118
+ current = queue.shift
119
+
120
+ yield current
121
+
122
+ if recursive
123
+ Dir.entries(current).each do |entry|
124
+ next if [".",".."].include? entry
125
+ to_queue = (current == "." ? entry : File.join(current, entry))
126
+
127
+ queue.push to_queue if FileTest.directory? to_queue
128
+ end
129
+ end
130
+
131
+ queue -= visited
132
+ visited.push current
133
+ end
134
+ end
135
+
136
+ def self.each_remote_info(dir)
137
+ self.each_folder(dir) do |folder|
138
+ filename = File.join(folder, RemoteInfo::FILENAME)
139
+ yield filename if FileTest.exists? filename
140
+ end
141
+ end
142
+ end
143
+
144
+ class ScpUtils
145
+ def self.remote(local, file)
146
+ user = file.user
147
+ port = file.port
148
+ host = file.host
149
+ path = file.path
150
+
151
+ userstring = user ? user + "@" : ""
152
+
153
+ remotepath = path
154
+ remotepath = File.join(remotepath, local) if local && local != "."
155
+
156
+ result = "#{userstring}#{host}:#{remotepath}"
157
+ end
158
+
159
+ def self.local(*candidates)
160
+ candidates.each do |c|
161
+ return c if c
162
+ end
163
+
164
+ return "."
165
+ end
166
+
167
+ def self.scp(from, to, dir, port=nil)
168
+ dirstring = dir ? "-r" : ""
169
+ portstring = port ? "-P " + port : ""
170
+
171
+ return "scp #{portstring} #{dirstring} \"#{from}\" \"#{to}\""
172
+ end
173
+ end
174
+
175
+ class CmdParser
176
+ @@data = {}
177
+ @@bindings = {}
178
+
179
+ def self.opt(key)
180
+ return @@data[key]
181
+ end
182
+
183
+ def self.set(key, value)
184
+ @@data[key] = value
185
+ end
186
+
187
+ def self.setup(cmd, *args)
188
+ parser = OptionParser.new do |opts|
189
+ opts.banner = "Usage: #{cmd} [options] #{args.join " "}"
190
+
191
+ @@opts = opts
192
+ yield opts
193
+ @@opts = nil
194
+
195
+ opts.on("-v", "--verbose", "Writes a lot of information") do
196
+ CmdParser.set :verbose, true
197
+ end
198
+
199
+ opts.on("-h", "--help", "Prints this help") do
200
+ puts opts
201
+ exit
202
+ end
203
+ end
204
+
205
+ parser.parse! ARGV
206
+
207
+ arguments = {}
208
+ for i in 0...args.size
209
+ arguments[args[i]] = ARGV[i]
210
+ end
211
+
212
+ return arguments
213
+ end
214
+
215
+ def self.autoset_flag(short, long, description, flag, incompatibles=[])
216
+ @@bindings[flag] = short
217
+ @@opts.on(short, long, description) do
218
+ incompatibles.each do |incompatible|
219
+ CmdParser.assert_not_set incompatible, short, @@bindings[incompatible]
220
+ end
221
+
222
+ CmdParser.set flag, true
223
+ end
224
+ end
225
+
226
+ def self.autoset_param(short, long, description, flag, incompatibles=[])
227
+ @@bindings[flag] = short
228
+ @@opts.on(short, long, description) do |v|
229
+ incompatibles.each do |incompatible|
230
+ CmdParser.assert_not_set incompatible, short, @@bindings[incompatible]
231
+ end
232
+
233
+ CmdParser.set flag, v
234
+ end
235
+ end
236
+
237
+ def self.assert
238
+ error_message = yield
239
+ if error_message
240
+ puts error_message
241
+ exit
242
+ end
243
+ end
244
+
245
+ def self.assert_not_set(forbidden_key, actual_param, forbidden_param, message="incompatible commands")
246
+ if CmdParser.opt(forbidden_key) != nil
247
+ puts "Do not use #{forbidden_param} with #{actual_param} (#{message}). Aborting."
248
+ exit
249
+ end
250
+ end
251
+ end
252
+
253
+ class ConsoleRunner
254
+ def remove_file(file)
255
+ File.unlink file
256
+ end
257
+
258
+ def username
259
+ return `echo $USER`.chomp
260
+ end
261
+
262
+ def run(cmd)
263
+ vputs "Executing command: #{cmd}"
264
+ stdout, stderr, status = Open3.capture3(cmd)
265
+ return [stdout, stderr, status]
266
+ end
267
+ end
268
+
269
+ class SimulatorRunner < ConsoleRunner
270
+ def remove_file(file)
271
+ puts "SIMULATE: removed #{file}"
272
+ end
273
+
274
+ def run(cmd)
275
+ puts "SIMULATE: #{cmd}"
276
+ end
277
+ end
278
+
279
+ def vputs *args
280
+ puts *args if CmdParser.opt :verbose
281
+ end
282
+
283
+ OS = ConsoleRunner.new
284
+ # OS = SimulatorRunner.new
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: remotesync
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Simone Scalabrino
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.8.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.1
33
+ description: Sync remote folders using ssh. Supports network namespaces in Linux.
34
+ email: s.scalabrino9@gmail.com
35
+ executables:
36
+ - rrinit
37
+ - rrpull
38
+ - rrpush
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - bin/rrinit
43
+ - bin/rrpull
44
+ - bin/rrpush
45
+ - lib/commons.rb
46
+ homepage: https://github.com/intersimone999/remotesync
47
+ licenses:
48
+ - GPL-3.0
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.7.3
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Sync remote folders using ssh
70
+ test_files: []