OSDN Git Service

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