OSDN Git Service

Add bandwidth limit option.
[osdn-codes/osdn-cli.git] / lib / osdn / cli / command / frs_upload.rb
1 module OSDN; module CLI; module Command
2   class FrsUpload < Base
3     def help
4       puts "#{$0} frs_upload [opts] [target_dir]"
5       puts "Options:"
6       puts "  -n --dry-run               Do noting (use with global -v to inspect)"
7       puts "  -p --project=<project>     Target project (numeric id or name)"
8       puts "     --package=<package-id>  Target package (numeric id)"
9       puts "     --release=<release-id>  Target release (numeric id)"
10       puts "  -v --visibility=<public|private|hidden>"
11       puts "                             Default visibility for newly created items"
12       puts "      --force-digest         Calc local file digest forcely"
13       puts "      --progress             Force to show upload progress"
14       puts "      --no-progress          Force to hide upload progress"
15       puts "      --bwlimit=RATE         Limit bandwidth (in KB)"
16     end
17
18     def run
19       update_token
20       opts = GetoptLong.new(
21         [ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ],
22         [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
23         [ '--package', GetoptLong::REQUIRED_ARGUMENT ],
24         [ '--release', GetoptLong::REQUIRED_ARGUMENT ],
25         [ '--visibility', '-v', GetoptLong::REQUIRED_ARGUMENT ],
26         [ '--force-digest', GetoptLong::NO_ARGUMENT],
27         [ '--progress', GetoptLong::NO_ARGUMENT],
28         [ '--no-progress', GetoptLong::NO_ARGUMENT],
29         [ '--bwlimit', GetoptLong::REQUIRED_ARGUMENT ],
30       )
31       opts.each do |opt, arg|
32         case opt
33         when '--project'
34           arg.empty? or
35             @target_proj = arg
36         when '--release'
37           arg.empty? or
38             @target_release = arg
39         when '--package'
40           arg.empty? or
41             @target_package = arg
42         when '--visibility'
43           unless %w(public private hidden).member?(arg)
44             logger.fatal "Invalid visibility status: #{arg}"
45             exit
46           end
47           @visibility = arg
48         when '--force-digest'
49           @force_digest = true
50         when '--dry-run'
51           @dry_run = true
52         when '--progress'
53           @show_progress = true
54         when '--no-progress'
55           @show_progress = false
56         when '--bwlimit'
57           arg.to_i != 0 and
58             OSDN::CLI._rate_limit = arg.to_i * 1024
59         end
60       end
61
62       (ARGV.empty? ? ['.'] : ARGV).each do |d|
63         @target_dir = Pathname.new(d)
64         process_target
65       end
66     end
67
68     def process_target
69       proj_info = api.get_project target_proj # check project existance
70
71       vars = load_variables(@target_dir)
72       parent_vars = load_variables(@target_dir.parent)
73
74       if @target_release || vars.release_id ||
75          parent_vars.package_id && !vars.release_id # new release case...
76         process_release(@target_dir)
77       elsif @target_package || vars.package_id
78         process_package(@target_dir)
79       else
80         Pathname.glob(@target_dir+'*').sort.each do |pdir|
81           process_package(pdir)
82         end
83       end
84     end
85
86     def self.description
87       "Upload local file tree and create package/release implicitly."
88     end
89
90     def process_package(pdir)
91       if cur_pkgid = load_variables(pdir).package_id
92         # check package existance on server
93         begin
94           api.get_package target_proj, target_package(pdir)
95         rescue OSDNClient::ApiError => e
96           begin
97             err = JSON.parse(e.response_body)
98           rescue
99             raise e
100           end
101           if err['status'] == 404
102             logger.warn "Package ##{cur_pkgid} has been deleted on server and local directory '#{pdir}' remains. You can delete the local directory or delete '#{pdir}/.osdn.vars' file to create a package again with new ID."
103             return false
104           end
105           raise e
106         end
107       else
108         logger.info "Createing new package '#{pdir.basename}'"
109         if @dry_run
110           pinfo = Hashie::Mash.new id: '(dry-run)', name: pdir.basename, url: '(dry-run)'
111         else
112           pinfo = api.create_package target_proj, pdir.basename, visibility: @visibility
113           update_variables pdir, package_id: pinfo.id
114         end
115         $stdout.puts "New package '#{pinfo.name}' has been created; #{pinfo.url}"
116       end
117
118       Pathname.glob(pdir + '*').sort.each do |rdir|
119         process_release(rdir)
120       end
121     end
122
123     def process_release(rdir)
124       if !rdir.directory?
125         logger.warn "Skip normal file '#{rdir}' in release level"
126         return false
127       end
128
129       vars = load_variables(rdir)
130       rinfo = nil
131       if vars.release_id
132         begin
133           rinfo = api.get_release target_proj, target_package(rdir), target_release(rdir)
134         rescue OSDNClient::ApiError => e
135           begin
136             err = JSON.parse(e.response_body)
137           rescue
138             raise e
139           end
140           if err['status'] == 404
141             logger.warn "Release ##{vars.release_id} has been deleted on server and local directory '#{rdir}' remains. You can delete the local directory or delete '#{rdir}/.osdn.vars' file to create a release again with new ID."
142             return false
143           end
144           raise e
145         end
146       else
147         logger.info "Createing new release '#{rdir.basename}'"
148         if @dry_run
149           rinfo = Hashie::Mash.new id: '(dry-run)', name: rdir.basename, url: '(dry-run)', files: []
150         else
151           rinfo = api.create_release target_proj, target_package(rdir), rdir.basename, visibility: @visibility
152           update_variables rdir, release_id: rinfo.id
153         end
154         $stdout.puts "New release '#{rinfo.name}' has been created; #{rinfo.url}"
155       end
156       Pathname.glob(rdir + '*').sort.each do |file|
157         process_file(file, rdir, rinfo)
158       end
159     end
160
161     def process_file(file, rdir, rinfo)
162       if file.directory?
163         logger.error "Skip direcotry #{file}"
164         return false
165       end
166
167       vars = load_variables(rdir)
168       digests = nil
169       if !@force_digest && vars.local_file_info &&
170          vars.local_file_info[file.basename.to_s]
171         finfo = vars.local_file_info[file.basename.to_s]
172         if finfo[:size] == file.size && finfo.mtime == file.mtime
173           digests = vars.local_file_info[file.basename.to_s].digests
174         end
175       end
176
177       unless digests
178         logger.info "Calculating digest for #{file}..."
179         digests = {
180           sha256: hexdigest(Digest::SHA256, file),
181           sha1:   hexdigest(Digest::SHA1, file),
182           md5:    hexdigest(Digest::MD5, file),
183         }
184         update_variables rdir, {local_file_info: {file.basename.to_s => {digests: digests, mtime: file.mtime, size: file.size}}}
185       end
186       if remote_f = rinfo.files.find { |f| f.name == file.basename.to_s }
187         if digests.find { |type, dig| dig != remote_f.send("digest_#{type}") }
188           logger.error "#{file} was changed from remote file! Please delete remote file before uploading new one."
189         end
190         logger.info "Skip already uploaded file '#{file}'"
191         return
192       end
193
194       logger.info "Uploading file #{file} (#{file.size} bytes)"
195       if @dry_run
196         finfo = Hashie::Mash.new id: '(dry-run)', url: '(dry-run)'
197       else
198         logger.level <= Logger::INFO && @show_progress != false || @show_progress and
199           OSDN::CLI._show_progress = true
200         fio = file.open
201         logger.info "Starting upload #{file}..."
202         finfo = api.create_release_file target_proj, target_package(rdir), target_release(rdir), fio, visibility: @visibility
203         fio.close
204         OSDN::CLI._show_progress = false
205         if digests.find { |type, dig| dig != finfo.send("digest_#{type}") }
206           logger.error "File digests are mismatch! Upload file #{file} may be broken! Please check."
207         else
208           logger.info "Upload complete."
209         end
210       end
211       $stdout.puts "New file '#{file}' has been uploaded; #{finfo.url}"
212     end
213     
214     private
215     def target_proj
216       @target_proj and return @target_proj
217       vars = load_variables(@target_dir)
218       vars.project && !vars.project.empty? and
219         return vars.project
220       logger.fatal "No target project is specified."
221       exit
222     end
223
224     def target_package(dir)
225       @target_package and return @target_package
226       vars = load_variables(dir)
227       vars.package_id && !vars.package_id.to_s.empty? and
228         return vars.package_id
229       logger.fatal "No target package is specified."
230       exit
231     end
232
233     def target_release(dir)
234       @target_release and return @target_release
235       vars = load_variables(dir)
236       vars.release_id && !vars.release_id.to_s.empty? and
237         return vars.release_id
238       logger.fatal "No target release is specified."
239       exit
240     end
241
242     def api
243       OSDNClient::ProjectApi.new
244     end
245   end
246 end; end; end