bukin 0.8.0 → 0.9.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.
- data/.travis.yml +1 -0
- data/README.md +2 -2
- data/lib/bukin.rb +1 -1
- data/lib/bukin/bukfile.rb +1 -1
- data/lib/bukin/cli.rb +50 -11
- data/lib/bukin/installer.rb +17 -12
- data/lib/bukin/state.rb +77 -0
- data/lib/bukin/version.rb +1 -1
- data/spec/state_spec.rb +72 -0
- metadata +5 -5
- data/lib/bukin/lockfile.rb +0 -31
- data/spec/lockfile_spec.rb +0 -78
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -40,12 +40,12 @@ Bukin is still a work in progress and is far from feature complete. Currently i
|
|
40
40
|
* Auto selection if none specified
|
41
41
|
* Automatic or user specified filtering of downloaded files
|
42
42
|
* Automatic or user specified extraction of zip files
|
43
|
+
* Automatic detection of already installed plugins
|
43
44
|
* Installation groups
|
44
45
|
|
45
46
|
Eventually, I'd like to add some of the following:
|
46
47
|
|
47
48
|
* A lockfile that tracks exactly what versions are installed
|
48
|
-
* Automatic detection of already installed plugins
|
49
49
|
* Dependency tracking and resolution
|
50
50
|
* More complex version selectors
|
51
51
|
* Modpack support
|
@@ -61,7 +61,7 @@ Usage
|
|
61
61
|
Bukin works by reading a list of dependencies from a `Bukfile`. The most basic usage would be:
|
62
62
|
|
63
63
|
```bash
|
64
|
-
|
64
|
+
bukin init
|
65
65
|
bukin install
|
66
66
|
```
|
67
67
|
|
data/lib/bukin.rb
CHANGED
@@ -48,7 +48,7 @@ module Bukin
|
|
48
48
|
|
49
49
|
def self.with_friendly_errors
|
50
50
|
yield
|
51
|
-
rescue
|
51
|
+
rescue BukinError => error
|
52
52
|
abort error.message
|
53
53
|
rescue SocketError => error
|
54
54
|
abort "#{error.message}\nCheck that you have a stable connection and the service is online"
|
data/lib/bukin/bukfile.rb
CHANGED
data/lib/bukin/cli.rb
CHANGED
@@ -7,6 +7,7 @@ require 'bukin/bukget'
|
|
7
7
|
require 'bukin/bukkit_dl'
|
8
8
|
require 'bukin/jenkins'
|
9
9
|
require 'bukin/download'
|
10
|
+
require 'bukin/state'
|
10
11
|
|
11
12
|
module Bukin
|
12
13
|
class CLI < Thor
|
@@ -18,13 +19,30 @@ module Bukin
|
|
18
19
|
without = options[:without].map(&:to_sym)
|
19
20
|
raw_resources = parse_resources(names, without)
|
20
21
|
|
21
|
-
# Get all the
|
22
|
+
# Get all the information needed to install
|
22
23
|
resources = prepare_resources(raw_resources)
|
23
24
|
|
24
25
|
# Download and install all resources
|
25
26
|
install_resources(resources)
|
26
27
|
end
|
27
28
|
|
29
|
+
desc 'init', "Creates a new Bukfile with craftbukkit as a server and adds"\
|
30
|
+
"the .bukin directory to any existing .gitignore file."
|
31
|
+
def init
|
32
|
+
path = File.join(Dir.pwd, Bukfile::FILE_NAME)
|
33
|
+
if File.exist?(path)
|
34
|
+
say 'Bukfile already exists'
|
35
|
+
else
|
36
|
+
say 'Creating Bukfile'
|
37
|
+
File.open(path, 'w') {|file| file.write("server 'craftbukkit'")}
|
38
|
+
end
|
39
|
+
|
40
|
+
if File.exist?('.gitignore') && File.readlines('.gitignore').grep(/\.bukin/).empty?
|
41
|
+
say 'Writing .bukin to gitignore'
|
42
|
+
File.open('.gitignore', 'a') {|file| file.write('.bukin')}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
28
46
|
def help(*)
|
29
47
|
shell.say "Bukin is a plugin and server package manager for Minecraft.\n"
|
30
48
|
super
|
@@ -32,7 +50,7 @@ module Bukin
|
|
32
50
|
|
33
51
|
private
|
34
52
|
def parse_resources(names, without)
|
35
|
-
resources = section('Parsing Bukfile') {
|
53
|
+
resources = section('Parsing Bukfile') {Bukfile.new.resources}
|
36
54
|
|
37
55
|
# If name are specified, only install resources with those names
|
38
56
|
resources.select! {|resource| names.include?(resource[:name])} if names.any?
|
@@ -43,7 +61,7 @@ module Bukin
|
|
43
61
|
resource[:group].empty? || (resource[:group] - without).any?
|
44
62
|
end
|
45
63
|
end
|
46
|
-
raise
|
64
|
+
raise BukinError, "Nothing to install" if resources.empty?
|
47
65
|
|
48
66
|
resources
|
49
67
|
end
|
@@ -59,13 +77,13 @@ module Bukin
|
|
59
77
|
end
|
60
78
|
|
61
79
|
downloads.each do |provider, datas|
|
62
|
-
|
80
|
+
fetching_section provider do
|
63
81
|
datas.each do |data|
|
64
82
|
begin
|
65
83
|
version, download = provider.find(data)
|
66
84
|
final_resources << Resource.new(data, version, download)
|
67
85
|
rescue OpenURI::HTTPError => ex
|
68
|
-
raise
|
86
|
+
raise BukinError,
|
69
87
|
"There was an error fetching information about "\
|
70
88
|
"'#{data[:name]} (#{data[:version]})'.\n"\
|
71
89
|
"#{ex.message}"
|
@@ -78,15 +96,24 @@ module Bukin
|
|
78
96
|
end
|
79
97
|
|
80
98
|
def install_resources(resources)
|
81
|
-
installer =
|
99
|
+
installer = Installer.new(Dir.pwd)
|
100
|
+
state = State.new(Dir.pwd)
|
82
101
|
|
102
|
+
# Install new resources
|
83
103
|
resources.each do |resource|
|
84
|
-
|
104
|
+
if state.files.installed?(resource.name, resource.version)
|
105
|
+
using_section(resource.name, resource.version)
|
106
|
+
next
|
107
|
+
else
|
108
|
+
state.files.delete(resource.name)
|
109
|
+
end
|
110
|
+
|
111
|
+
installing_section resource.name, resource.version do
|
85
112
|
begin
|
86
113
|
installer.install(resource)
|
87
114
|
rescue OpenURI::HTTPError => ex
|
88
115
|
raise(
|
89
|
-
|
116
|
+
BukinError,
|
90
117
|
"There was an error installing "\
|
91
118
|
"'#{resource[:name]} (#{resource[:version]})'.\n"\
|
92
119
|
"#{ex.message}"
|
@@ -94,6 +121,12 @@ module Bukin
|
|
94
121
|
end
|
95
122
|
end
|
96
123
|
end
|
124
|
+
|
125
|
+
# Remove old resources
|
126
|
+
names = resources.map(&:name)
|
127
|
+
state.files.names.each do |name|
|
128
|
+
state.files.delete(name) unless names.include?(name)
|
129
|
+
end
|
97
130
|
end
|
98
131
|
|
99
132
|
PROVIDERS = {
|
@@ -131,13 +164,19 @@ module Bukin
|
|
131
164
|
raise ex
|
132
165
|
end
|
133
166
|
|
134
|
-
def
|
135
|
-
msg = "
|
167
|
+
def installing_section(name, version, &block)
|
168
|
+
msg = "Installing #{name}"
|
136
169
|
msg << " (#{version})" if version
|
137
170
|
section(msg, &block)
|
138
171
|
end
|
139
172
|
|
140
|
-
def
|
173
|
+
def using_section(name, version)
|
174
|
+
msg = "Using #{name}"
|
175
|
+
msg << " (#{version})" if version
|
176
|
+
section(msg) {}
|
177
|
+
end
|
178
|
+
|
179
|
+
def fetching_section(provider, &block)
|
141
180
|
if provider.is_a?(Download)
|
142
181
|
yield
|
143
182
|
else
|
data/lib/bukin/installer.rb
CHANGED
@@ -1,27 +1,29 @@
|
|
1
|
-
require 'bukin/
|
1
|
+
require 'bukin/state'
|
2
2
|
require 'zip'
|
3
3
|
|
4
4
|
module Bukin
|
5
5
|
class Installer
|
6
6
|
PATHS = { :server => '.', :plugin => 'plugins' }
|
7
7
|
|
8
|
-
def initialize(path
|
9
|
-
@
|
8
|
+
def initialize(path)
|
9
|
+
@state = State.new(path)
|
10
10
|
end
|
11
11
|
|
12
12
|
def install(resource)
|
13
13
|
path = PATHS[resource.type]
|
14
|
-
|
14
|
+
files = []
|
15
15
|
dl_data, dl_name = download_file(resource.download)
|
16
16
|
|
17
17
|
if File.extname(dl_name) == '.zip'
|
18
18
|
match = self.get_match(resource[:extract])
|
19
|
-
|
20
|
-
raise
|
19
|
+
files = extract_files(dl_data, path, match)
|
20
|
+
raise InstallError, "The resource #{resource.name} (#{resources.version}) has no jar files in it's download (zip file)." if files.empty?
|
21
21
|
else
|
22
|
-
save_download(dl_data, dl_name, path)
|
23
|
-
file_names << dl_name
|
22
|
+
files = save_download(dl_data, dl_name, path)
|
24
23
|
end
|
24
|
+
|
25
|
+
@state.files[resource.name, resource.version] = files
|
26
|
+
@state.save
|
25
27
|
end
|
26
28
|
|
27
29
|
def extract_files(file_data, path, match)
|
@@ -34,8 +36,9 @@ module Bukin
|
|
34
36
|
Zip::File.open(tempfile.path) do |zipfile|
|
35
37
|
files = zipfile.find_all {|file| file.name =~ match}
|
36
38
|
files.each do |file|
|
37
|
-
|
38
|
-
|
39
|
+
full_path = File.join(path, file.name)
|
40
|
+
file.extract(full_path) { true }
|
41
|
+
file_names << full_path
|
39
42
|
end
|
40
43
|
end
|
41
44
|
ensure
|
@@ -47,9 +50,11 @@ module Bukin
|
|
47
50
|
|
48
51
|
def save_download(data, name, path)
|
49
52
|
FileUtils.mkdir_p(path)
|
50
|
-
|
53
|
+
full_path = File.join(path, name)
|
54
|
+
open(full_path, 'wb') do |file|
|
51
55
|
file.print data
|
52
56
|
end
|
57
|
+
[full_path]
|
53
58
|
end
|
54
59
|
|
55
60
|
def download_file(url, content_disposition = false)
|
@@ -76,7 +81,7 @@ module Bukin
|
|
76
81
|
when nil
|
77
82
|
/\.jar$/
|
78
83
|
else
|
79
|
-
raise
|
84
|
+
raise InstallError, "The extract option #{match} is not valid. Please use a String, Regexp or :all"
|
80
85
|
end
|
81
86
|
end
|
82
87
|
|
data/lib/bukin/state.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Bukin
|
5
|
+
# State of the current install
|
6
|
+
class State
|
7
|
+
attr_reader :path, :files
|
8
|
+
|
9
|
+
def initialize(path = nil)
|
10
|
+
path ||= '.'
|
11
|
+
@path = File.join(path, '.bukin')
|
12
|
+
create_dir
|
13
|
+
@files = FileState.new(File.join(@path, 'files.yml'), path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def save
|
17
|
+
create_dir
|
18
|
+
@files.save
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def create_dir
|
23
|
+
FileUtils.mkdir_p(@path) unless Dir.exist?(@path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class FileState
|
28
|
+
def initialize(path, base_path)
|
29
|
+
@path = path
|
30
|
+
@base_path = base_path
|
31
|
+
@files = (File.exist?(@path) ? YAML::load_file(@path) : {})
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](name, version = nil)
|
35
|
+
if version
|
36
|
+
(@files[name] || {})[version]
|
37
|
+
else
|
38
|
+
@files[name]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def []=(name, version = nil, value)
|
43
|
+
if version
|
44
|
+
(@files[name] ||= {})[version] = value
|
45
|
+
else
|
46
|
+
@files[name] = value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def installed?(name, version)
|
51
|
+
files = self[name, version]
|
52
|
+
return false unless files
|
53
|
+
|
54
|
+
files.all? do |file|
|
55
|
+
File.exist?(File.join(@base_path, file))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete(name)
|
60
|
+
(self[name] || {}).each do |version, files|
|
61
|
+
files.each do |file|
|
62
|
+
FileUtils.rm_r(file) if File.exist?(file)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
@files.delete(name)
|
67
|
+
end
|
68
|
+
|
69
|
+
def names
|
70
|
+
@files.keys
|
71
|
+
end
|
72
|
+
|
73
|
+
def save
|
74
|
+
File.open(@path, "w") {|file| file.write @files.to_yaml}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/bukin/version.rb
CHANGED
data/spec/state_spec.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'bukin/state'
|
3
|
+
require 'fakefs/safe'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
describe Bukin::State do
|
8
|
+
before(:all) do
|
9
|
+
FakeFS.activate!
|
10
|
+
@path = Bukin::State.new.path
|
11
|
+
end
|
12
|
+
|
13
|
+
before(:each) {FileUtils.rm_r(@path) if Dir.exist?(@path)}
|
14
|
+
after(:all) {FakeFS.deactivate!}
|
15
|
+
|
16
|
+
it 'assignes a default path if none is provided' do
|
17
|
+
state = Bukin::State.new
|
18
|
+
state.path.should == @path
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'assignes the path provided in the constructor' do
|
22
|
+
state = Bukin::State.new('/test/path')
|
23
|
+
state.path.should == '/test/path/.bukin'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'stores files by their name and version' do
|
27
|
+
state = Bukin::State.new
|
28
|
+
state.files['worldguard', '1.0.0'] = ['plugins/WorldGuard.jar']
|
29
|
+
|
30
|
+
state.files['worldguard'].should == { '1.0.0' => ['plugins/WorldGuard.jar'] }
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'saves files to a yaml file' do
|
34
|
+
state = Bukin::State.new
|
35
|
+
state.files['worldguard', '1.0.0'] = ['plugins/WorldGuard.jar']
|
36
|
+
state.save
|
37
|
+
|
38
|
+
data = YAML::load_file(File.join(state.path,'files.yml'))
|
39
|
+
data.should == { 'worldguard' => { '1.0.0' => ['plugins/WorldGuard.jar'] } }
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'loads files from a yaml file' do
|
43
|
+
data = { 'worldguard' => { '1.0.0' => ['plugins/WorldGuard.jar'] } }
|
44
|
+
FileUtils.mkdir_p(@path)
|
45
|
+
File.open(File.join(@path, 'files.yml'), 'w') {|file| file.write data.to_yaml}
|
46
|
+
|
47
|
+
state = Bukin::State.new
|
48
|
+
state.files['worldguard', '1.0.0'].should == ['plugins/WorldGuard.jar']
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'checks for already instaled files' do
|
52
|
+
FileUtils.mkdir_p('plugins')
|
53
|
+
FileUtils.touch('plugins/WorldGuard.jar')
|
54
|
+
|
55
|
+
state = Bukin::State.new
|
56
|
+
state.files['worldguard', '1.0.0'] = ['plugins/WorldGuard.jar']
|
57
|
+
|
58
|
+
state.files.installed?('worldguard', '1.0.0').should be_true
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'removes old installed files' do
|
62
|
+
FileUtils.mkdir_p('plugins')
|
63
|
+
FileUtils.touch('plugins/WorldGuard.jar')
|
64
|
+
|
65
|
+
state = Bukin::State.new
|
66
|
+
state.files['worldguard', '1.0.0'] = ['plugins/WorldGuard.jar']
|
67
|
+
state.files.delete('worldguard')
|
68
|
+
|
69
|
+
File.exist?('plugins/WorldGuard.jar').should be_false
|
70
|
+
state.files['worldguard', '1.0.0'].should be_nil
|
71
|
+
end
|
72
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bukin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-01-
|
12
|
+
date: 2014-01-24 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -70,8 +70,8 @@ files:
|
|
70
70
|
- lib/bukin/file_match.rb
|
71
71
|
- lib/bukin/installer.rb
|
72
72
|
- lib/bukin/jenkins.rb
|
73
|
-
- lib/bukin/lockfile.rb
|
74
73
|
- lib/bukin/resource.rb
|
74
|
+
- lib/bukin/state.rb
|
75
75
|
- lib/bukin/version.rb
|
76
76
|
- spec/bukfile_spec.rb
|
77
77
|
- spec/bukget_spec.rb
|
@@ -97,8 +97,8 @@ files:
|
|
97
97
|
- spec/file_match_spec.rb
|
98
98
|
- spec/installer_spec.rb
|
99
99
|
- spec/jenkins_spec.rb
|
100
|
-
- spec/lockfile_spec.rb
|
101
100
|
- spec/spec_helper.rb
|
101
|
+
- spec/state_spec.rb
|
102
102
|
homepage: http://github.com/Nullreff/bukin
|
103
103
|
licenses: []
|
104
104
|
post_install_message:
|
@@ -148,6 +148,6 @@ test_files:
|
|
148
148
|
- spec/file_match_spec.rb
|
149
149
|
- spec/installer_spec.rb
|
150
150
|
- spec/jenkins_spec.rb
|
151
|
-
- spec/lockfile_spec.rb
|
152
151
|
- spec/spec_helper.rb
|
152
|
+
- spec/state_spec.rb
|
153
153
|
has_rdoc:
|
data/lib/bukin/lockfile.rb
DELETED
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
|
3
|
-
module Bukin
|
4
|
-
class Lockfile
|
5
|
-
FILE_NAME = 'Bukfile.lock'
|
6
|
-
attr_reader :path
|
7
|
-
|
8
|
-
def initialize(path = nil)
|
9
|
-
@path = path || File.join(Dir.pwd, FILE_NAME)
|
10
|
-
@resources = File.exist?(@path) ? YAML::load_file(@path) : {}
|
11
|
-
@resources = { 'resources' => {} } if @resources['resources'].nil?
|
12
|
-
end
|
13
|
-
|
14
|
-
def add(data)
|
15
|
-
name = data[:name]
|
16
|
-
resources[name] = {
|
17
|
-
'version' => data[:version],
|
18
|
-
'files' => data[:files]
|
19
|
-
}
|
20
|
-
save
|
21
|
-
end
|
22
|
-
|
23
|
-
def resources
|
24
|
-
@resources['resources']
|
25
|
-
end
|
26
|
-
|
27
|
-
def save
|
28
|
-
File.open(@path, "w") {|file| file.write @resources.to_yaml}
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
data/spec/lockfile_spec.rb
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'bukin'
|
3
|
-
require 'fakefs/safe'
|
4
|
-
require 'yaml'
|
5
|
-
|
6
|
-
describe Bukin::Lockfile do
|
7
|
-
before(:all) do
|
8
|
-
FakeFS.activate!
|
9
|
-
@path = File.join(Dir.pwd, Bukin::Lockfile::FILE_NAME)
|
10
|
-
end
|
11
|
-
|
12
|
-
before(:each) {File.delete(@path) if File.exist?(@path)}
|
13
|
-
after(:all) {FakeFS.deactivate!}
|
14
|
-
|
15
|
-
it 'assignes a default path if none is provided' do
|
16
|
-
lockfile = Bukin::Lockfile.new
|
17
|
-
lockfile.path.should == @path
|
18
|
-
end
|
19
|
-
|
20
|
-
it 'assignes the path provided in the constructor' do
|
21
|
-
lockfile = Bukin::Lockfile.new('/test/path')
|
22
|
-
lockfile.path.should == '/test/path'
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'loads no resources for an empty or missing file' do
|
26
|
-
lockfile = Bukin::Lockfile.new('/non/existant/path')
|
27
|
-
lockfile.resources.should == {}
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'loads resources from an already existing lockfile' do
|
31
|
-
resources = { 'resources' => 'value' }
|
32
|
-
|
33
|
-
File.open(@path, 'w') {|file| file.write resources.to_yaml}
|
34
|
-
|
35
|
-
lockfile = Bukin::Lockfile.new
|
36
|
-
lockfile.resources.should == 'value'
|
37
|
-
end
|
38
|
-
|
39
|
-
it 'saves resources to a lockfile' do
|
40
|
-
lockfile = Bukin::Lockfile.new
|
41
|
-
lockfile.add({
|
42
|
-
:name => 'resource_name',
|
43
|
-
:version => '1.0.0',
|
44
|
-
:files => ['file']
|
45
|
-
})
|
46
|
-
lockfile.save
|
47
|
-
|
48
|
-
path = File.join
|
49
|
-
data = YAML::load_file(@path)
|
50
|
-
data.should == {
|
51
|
-
'resources' => {
|
52
|
-
'resource_name' => {
|
53
|
-
'version' => '1.0.0',
|
54
|
-
'files' => ['file']
|
55
|
-
}
|
56
|
-
}
|
57
|
-
}
|
58
|
-
|
59
|
-
File.delete(@path)
|
60
|
-
lockfile = Bukin::Lockfile.new
|
61
|
-
lockfile.resources.should == {}
|
62
|
-
end
|
63
|
-
|
64
|
-
it 'adds resources to the lockfile' do
|
65
|
-
lockfile = Bukin::Lockfile.new
|
66
|
-
lockfile.resources.count.should == 0
|
67
|
-
lockfile.add({
|
68
|
-
:name => 'resource_name',
|
69
|
-
:version => '1.0.0',
|
70
|
-
:files => ['file1', 'file2']
|
71
|
-
})
|
72
|
-
lockfile.resources.count.should == 1
|
73
|
-
lockfile.resources['resource_name'].should == {
|
74
|
-
'version' => '1.0.0',
|
75
|
-
'files' => ['file1', 'file2']
|
76
|
-
}
|
77
|
-
end
|
78
|
-
end
|