puts "Options:"
puts " -n --dry-run Do noting (use with global -v to inspect)"
puts " -p --project=<project> Target project (numeric id or name)"
- #puts " --package=<project> Target package (numeric id)"
- #puts " --release=<project> Target release (numeric id)"
+ puts " --package=<package-id> Target package (numeric id)"
+ puts " --release=<release-id> Target release (numeric id)"
puts " -v --visibility=<public|private|hidden>"
puts " Default visibility for newly created items"
+ puts " --force-digest Calc local file digest forcely"
+ puts " --progress Force to show upload progress"
+ puts " --no-progress Force to hide upload progress"
+ puts " --bwlimit=RATE Limit bandwidth (in KB)"
end
def run
opts = GetoptLong.new(
[ '--dry-run', '-n', GetoptLong::NO_ARGUMENT ],
[ '--project', '-p', GetoptLong::REQUIRED_ARGUMENT ],
- [ '--release', '-r', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--package', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--release', GetoptLong::REQUIRED_ARGUMENT ],
[ '--visibility', '-v', GetoptLong::REQUIRED_ARGUMENT ],
+ [ '--force-digest', GetoptLong::NO_ARGUMENT],
+ [ '--progress', GetoptLong::NO_ARGUMENT],
+ [ '--no-progress', GetoptLong::NO_ARGUMENT],
+ [ '--bwlimit', GetoptLong::REQUIRED_ARGUMENT ],
)
opts.each do |opt, arg|
case opt
when '--project'
arg.empty? or
@target_proj = arg
- #when '--release'
- # arg.empty? or
- # @target_release = arg
- #when '--package'
- # arg.empty? or
- # @target_package = arg
+ when '--release'
+ arg.empty? or
+ @target_release = arg
+ when '--package'
+ arg.empty? or
+ @target_package = arg
when '--visibility'
unless %w(public private hidden).member?(arg)
logger.fatal "Invalid visibility status: #{arg}"
exit
end
@visibility = arg
+ when '--force-digest'
+ @force_digest = true
when '--dry-run'
@dry_run = true
+ when '--progress'
+ @show_progress = true
+ when '--no-progress'
+ @show_progress = false
+ when '--bwlimit'
+ arg.to_i != 0 and
+ OSDN::CLI._rate_limit = arg.to_i * 1024
end
end
- @target_dir = Pathname.new(ARGV.shift || '.')
+ (ARGV.empty? ? ['.'] : ARGV).each do |d|
+ @target_dir = Pathname.new(d)
+ process_target
+ end
+ end
+
+ def process_target
proj_info = api.get_project target_proj # check project existance
- Pathname.glob(@target_dir+'*').each do |pdir|
- unless load_variables(pdir).package_id
- logger.info "Createing new package '#{pdir.basename}'"
+ vars = load_variables(@target_dir)
+ parent_vars = load_variables(@target_dir.parent)
+
+ if @target_release || vars.release_id ||
+ parent_vars.package_id && !vars.release_id # new release case...
+ process_release(@target_dir)
+ elsif @target_package || vars.package_id
+ process_package(@target_dir)
+ else
+ Pathname.glob(@target_dir+'*').sort.each do |pdir|
+ process_package(pdir)
+ end
+ end
+ end
+
+ def self.description
+ "Upload local file tree and create package/release implicitly."
+ end
+
+ def process_package(pdir)
+ if cur_pkgid = load_variables(pdir).package_id
+ # check package existance on server
+ begin
+ api.get_package target_proj, target_package(pdir)
+ rescue OSDNClient::ApiError => e
+ begin
+ err = JSON.parse(e.response_body)
+ rescue
+ raise e
+ end
+ if err['status'] == 404
+ 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."
+ return false
+ end
+ raise e
+ end
+ else
+ logger.info "Createing new package '#{pdir.basename}'"
+ if @dry_run
+ pinfo = Hashie::Mash.new id: '(dry-run)', name: pdir.basename, url: '(dry-run)'
+ else
pinfo = api.create_package target_proj, pdir.basename, visibility: @visibility
update_variables pdir, package_id: pinfo.id
- $stdout.puts "New package '#{pinfo.name}' has been created; #{pinfo.url}"
end
+ $stdout.puts "New package '#{pinfo.name}' has been created; #{pinfo.url}"
+ end
+
+ Pathname.glob(pdir + '*').sort.each do |rdir|
+ process_release(rdir)
+ end
+ end
+
+ def process_release(rdir)
+ if !rdir.directory?
+ logger.warn "Skip normal file '#{rdir}' in release level"
+ return false
+ end
- Pathname.glob(pdir + '*').each do |rdir|
- vars = load_variables(rdir)
- rinfo = nil
- if vars.release_id
- rinfo = api.get_release target_proj, target_package(rdir), target_release(rdir)
- else vars.release_id
- logger.info "Createing new release '#{rdir.basename}'"
- rinfo = nil
- if api.respond_to? :create_reelase # TODO: remove, just typo...
- rinfo = api.create_reelase target_proj, target_package(rdir), rdir.basename, visibility: @visibility
- else
- rinfo = api.create_release target_proj, target_package(rdir), rdir.basename, visibility: @visibility
- end
- update_variables rdir, release_id: rinfo.id
- $stdout.puts "New release '#{rinfo.name}' has been created; #{rinfo.url}"
+ vars = load_variables(rdir)
+ rinfo = nil
+ if vars.release_id
+ begin
+ rinfo = api.get_release target_proj, target_package(rdir), target_release(rdir)
+ rescue OSDNClient::ApiError => e
+ begin
+ err = JSON.parse(e.response_body)
+ rescue
+ raise e
end
-
- Pathname.glob(rdir + '*').each do |file|
- if file.directory?
- logger.error "Skip direcotry #{file}"
- next
- end
-
- if rinfo.files.find { |f| f.name == file.basename.to_s }
- logger.warn "Skip already uploaded file '#{file}'"
- else
- logger.info "Uploading file #{file} (#{file.size} bytes)"
- # TODO: show progress bar!
- finfo = api.create_release_file target_proj, target_package(rdir), target_release(rdir), file.open, visibility: @visibility
- logger.info "Upload completed."
- $stdout.puts "New file '#{file}' has been uploaded; #{finfo.url}"
- end
+ if err['status'] == 404
+ 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."
+ return false
end
+ raise e
end
+ else
+ logger.info "Createing new release '#{rdir.basename}'"
+ if @dry_run
+ rinfo = Hashie::Mash.new id: '(dry-run)', name: rdir.basename, url: '(dry-run)', files: []
+ else
+ rinfo = api.create_release target_proj, target_package(rdir), rdir.basename, visibility: @visibility
+ update_variables rdir, release_id: rinfo.id
+ end
+ $stdout.puts "New release '#{rinfo.name}' has been created; #{rinfo.url}"
+ end
+ Pathname.glob(rdir + '*').sort.each do |file|
+ process_file(file, rdir, rinfo)
end
end
- def self.description
- "Upload local file tree and create package/release implicitly."
- end
+ def process_file(file, rdir, rinfo)
+ if file.directory?
+ logger.error "Skip direcotry #{file}"
+ return false
+ end
+ vars = load_variables(rdir)
+ digests = nil
+ if !@force_digest && vars.local_file_info &&
+ vars.local_file_info[file.basename.to_s]
+ finfo = vars.local_file_info[file.basename.to_s]
+ if finfo[:size] == file.size && finfo.mtime == file.mtime
+ digests = vars.local_file_info[file.basename.to_s].digests
+ end
+ end
+
+ unless digests
+ logger.info "Calculating digest for #{file}..."
+ digests = {
+ sha256: hexdigest(Digest::SHA256, file),
+ sha1: hexdigest(Digest::SHA1, file),
+ md5: hexdigest(Digest::MD5, file),
+ }
+ update_variables rdir, {local_file_info: {file.basename.to_s => {digests: digests, mtime: file.mtime, size: file.size}}}
+ end
+ if remote_f = rinfo.files.find { |f| f.name == file.basename.to_s }
+ if digests.find { |type, dig| dig != remote_f.send("digest_#{type}") }
+ logger.error "#{file} was changed from remote file! Please delete remote file before uploading new one."
+ end
+ logger.info "Skip already uploaded file '#{file}'"
+ return
+ end
+
+ logger.info "Uploading file #{file} (#{file.size} bytes)"
+ if @dry_run
+ finfo = Hashie::Mash.new id: '(dry-run)', url: '(dry-run)'
+ else
+ logger.level <= Logger::INFO && @show_progress != false || @show_progress and
+ OSDN::CLI._show_progress = true
+ fio = file.open
+ logger.info "Starting upload #{file}..."
+ max_upload_tries = 5
+ upload_tries = 0
+ begin
+ upload_tries += 1
+ finfo = api.create_release_file target_proj, target_package(rdir), target_release(rdir), fio, visibility: @visibility
+ rescue OSDNClient::ApiError => e
+ if max_upload_tries - upload_tries <= 0
+ logger.error "Max upload attempts (#{max_upload_tries}) has been exceeded, give up!"
+ raise e
+ elsif [0, 100, 502].member?(e.code.to_i)
+ fio.rewind
+ logger.error "Upload error (#{e.code} #{e.message}), retrying (#{upload_tries}/#{max_upload_tries})..."
+ sleep 10
+ retry
+ else
+ raise e
+ end
+ ensure
+ OSDN::CLI._show_progress = false
+ fio.close
+ end
+ if digests.find { |type, dig| dig != finfo.send("digest_#{type}") }
+ logger.error "File digests are mismatch! Upload file #{file} may be broken! Please check."
+ else
+ logger.info "Upload complete."
+ end
+ end
+ $stdout.puts "New file '#{file}' has been uploaded; #{finfo.url}"
+ end
+
private
def target_proj
@target_proj and return @target_proj