OSDN Git Service

Add retry on file upload.
[osdn-codes/osdn-cli.git] / lib / osdn / cli / command / relfile.rb
1 require 'osdn/cli/command/frs_base'
2 require 'pathname'
3
4 module OSDN; module CLI; module Command
5   class Relfile < FrsBase
6     def help
7       puts "#{$0} relfile [opts] [list]"
8       puts "#{$0} relfile [opts] create <target-file> [target-files...]"
9       puts "#{$0} relfile [opts] update <numeric-file-id>"
10       puts "#{$0} relfile [opts] delete <numeric-file-id>"
11       puts "Options:"
12       puts "  -f --format=<pretty|json>  Set output format"
13       puts "  -p --project=<project>     Target project (numeric id or name)"
14       puts "     --package=<package-id>  Target package (numeric id)"
15       puts "     --release=<release-id>  Target release (numeric id)"
16       puts "  -v --visibility=<public|private|hidden>"
17       puts "      --force-digest         Calc local file digest forcely"
18       puts "      --progress             Force to show upload progress"
19       puts "      --no-progress          Force to hide upload progress"
20       puts "      --bwlimit=RATE         Limit bandwidth (in KB)"
21     end
22
23     def self.description
24       "Manipulate frs files of project"
25     end
26
27     def process_options
28       opts = GetoptLong.new(
29         [ '--format', '-f', GetoptLong::REQUIRED_ARGUMENT ],
30         [ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
31         [ '--package', GetoptLong::REQUIRED_ARGUMENT ],
32         [ '--release', GetoptLong::REQUIRED_ARGUMENT ],
33         [ '--visibility', '-v', GetoptLong::REQUIRED_ARGUMENT ],
34         [ '--force-digest', GetoptLong::NO_ARGUMENT],
35         [ '--progress', GetoptLong::NO_ARGUMENT],
36         [ '--no-progress', GetoptLong::NO_ARGUMENT],
37         [ '--bwlimit', GetoptLong::REQUIRED_ARGUMENT ],
38       )
39       opts.each do |opt, arg|
40         case opt
41         when '--format'
42           arg == 'json' and
43             self.format = arg
44         when '--project'
45           arg.empty? or
46             @target_proj = arg
47         when '--package'
48           arg.empty? or
49             @target_package = arg
50         when '--release'
51           arg.empty? or
52             @target_release = arg
53         when '--force-digest'
54           @force_digest = true
55         when '--visibility'
56           unless %w(public private hidden).member?(arg)
57             logger.fatal "Invalid visibility status: #{arg}"
58             exit
59           end
60           @visibility = arg
61         when '--progress'
62           @show_progress = true
63         when '--no-progress'
64           @show_progress = false
65         when '--bwlimit'
66           arg.to_i != 0 and
67             OSDN::CLI._rate_limit = arg.to_i * 1024
68         end
69       end
70     end
71     
72     def list
73       release = api.get_release target_proj, target_package, target_release
74       list = release.files
75       if format == 'json'
76         puts list.map{|i| i.to_hash}.to_json
77       else
78         list.each do |f|
79           puts format_file(f)
80         end
81       end
82     end
83
84     def create
85       if ARGV.empty? || ARGV.first == ""
86         logger.fatal "Target filename is missing."
87         help
88         return
89       end
90
91       ARGV.each do |f|
92         create_one(f)
93       end
94     end
95
96     def create_one(filename)
97       file = Pathname('.') + filename
98       logger.debug "Calculating digest for #{file}..."
99
100       vars = load_variables(file.dirname)
101       digests = nil
102       if !@force_digest && vars.local_file_info &&
103          vars.local_file_info[file.basename.to_s]
104         finfo = vars.local_file_info[file.basename.to_s]
105         if finfo[:size] == file.size && finfo.mtime == file.mtime
106           digests = vars.local_file_info[file.basename.to_s].digests
107         end
108       end
109
110       unless digests
111         logger.info "Calculating digest for #{file}..."
112         digests = {
113           sha256: hexdigest(Digest::SHA256, file),
114           sha1:   hexdigest(Digest::SHA1, file),
115           md5:    hexdigest(Digest::MD5, file),
116         }
117         update_variables file.dirname, {local_file_info: {file.basename.to_s => {digests: digests, mtime: file.mtime, size: file.size}}}
118       end
119
120       fio = file.open
121       logger.level <= Logger::INFO && @show_progress != false || @show_progress and
122         OSDN::CLI._show_progress = true
123       logger.info "Starting upload #{file}..."
124       max_upload_tries = 5
125       upload_tries = 0
126       f = nil
127       begin
128         upload_tries += 1
129         f = api.create_release_file target_proj, target_package, target_release, fio, visibility: @visibility
130       rescue OSDNClient::ApiError => e
131         if max_upload_tries - upload_tries <= 0 
132           logger.error "Max upload attempts (#{max_upload_tries}) has been exceeded, give up!"
133           raise e
134         elsif [0, 100, 502].member?(e.code.to_i)
135           fio.rewind
136           logger.error "Upload error (#{e.code} #{e.message}), retrying (#{upload_tries}/#{max_upload_tries})..."
137           sleep 10
138           retry
139         else
140           raise e
141         end
142       ensure
143         fio.close
144         OSDN::CLI._show_progress = false
145       end
146       
147       if digests.find { |type, dig| dig != f.send("digest_#{type}") }
148         logger.error "File digests are mismatch! Upload file #{file} may be broken! Please check."
149       else
150         logger.info "Upload complete."
151       end
152       puts format_file(f)
153     end
154
155     def update
156       target_id = ARGV.shift
157       if !target_id
158         logger.fatal "Target file ID is missing."
159         help
160         return
161       end
162       if @visibility.nil?
163         logger.fatal "Visibility status is missing. Use '-v <public|private|hidden>'."
164         return
165       end
166       f = api.update_release_file target_proj, target_package, target_release, target_id, visibility: @visibility
167       logger.info "file #{target_id} has been updated."
168       puts format_file(f)
169     end
170
171     def delete
172       target_id = ARGV.shift
173       if !target_id
174         logger.fatal "Target file ID is missing."
175         help
176         return
177       end
178       f = api.delete_release_file target_proj, target_package, target_release, target_id
179       logger.info "file #{target_id} has been deleted."
180     end
181   end
182 end; end; end