OSDN Git Service

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