#!/usr/bin/env ruby # coding: utf-8 require 'bundler/setup' require 'thor' require 'yaml' require 'cos' class COS_CLI < Thor DEFAULT_CONFIG = '~/.cos.yml' package_name 'COS Ruby SDK CLI' class_option :config, aliases: '-c', default: DEFAULT_CONFIG, desc: '加载配置文件' class_option :bucket, aliases: '-b', desc: '指定Bucket' desc 'list [PATH]', '获取目录列表' method_option :prefix, aliases: '-p', desc: '前缀搜索' method_option :num, aliases: '-n', desc: '每页拉取的数量' method_option :pattern, aliases: '-t', desc: '获取方式', banner: 'file, dir, both' method_option :order, aliases: '-o', desc: '排序方式', banner: 'asc, desc' def list(path = '') rescue_errors do bucket.list(path, enabled_options([:prefix, :num, :pattern, :order])).each do |res| if res.type == 'file' puts(res.path, :blue) else puts(res.path) end end end end map ls: :list desc 'create_folder [PATH]', '创建目录' method_option :biz_attr, aliases: '-r', desc: '业务属性' def create_folder(path) rescue_errors do dir = bucket.create_folder(path, enabled_options([:biz_attr])) puts("#{dir.path} 目录创建成功!", :purple) end end map mkdir: :create_folder desc 'stat [PATH]', '获取目录或文件信息' def stat(path = '') rescue_errors do stat = bucket.stat(path) format_puts_stat(stat.to_hash.to_s) end end desc 'tree [PATH]', '显示树形结构' method_option :depth, aliases: '-d', desc: '目录深度', type: :numeric, default: 5 method_option :files_count, aliases: '-f', desc: '显示文件个数', type: :boolean, default: false method_option :files, aliases: '-a', desc: '列出文件', type: :boolean, default: false def tree(path = '') rescue_errors do path_obj = bucket.stat(path) COS::Tree.new( enabled_options([:depth, :files_count, :files]).merge({path: path_obj}) ).print_tree end end desc 'upload [PATH] [FILE_NAME] [FILE_SRC]', '上传文件(大文件自动分片上传,支持多线程上传,断点续传)' method_option :biz_attr, aliases: '-r', desc: '业务属性' method_option :min_slice_size, aliases: '-m', desc: '最小完整上传大小', type: :numeric, banner: 'bytes' method_option :auto_create_folder, aliases: '-f', desc: '自动创建目录', type: :boolean, default: false method_option :disable_cpt, aliases: '-d', desc: '禁用断点续传(分片上传时有效)', type: :boolean, default: false method_option :threads, aliases: '-t', desc: '线程数(分片上传时有效)', type: :numeric method_option :upload_retry, aliases: '-n', desc: '重试次数(分片上传时有效)', type: :numeric method_option :slice_size, aliases: '-s', desc: '分片上传时每个分片的大小(分片上传时有效)', type: :numeric method_option :cpt_file, aliases: '-e', desc: '指定断点续传记录(分片上传时有效)' def upload(path, file_name, file_src) rescue_errors do file = bucket.upload(path, file_name, file_src, enabled_options([:biz_attr, :min_slice_size, :auto_create_folder, :disable_cpt, :threads, :upload_retry, :slice_size, :cpt_file])) do |percent| puts("上传进度: #{(percent*100).round(2)}%", :green) end format_puts_stat(file.to_hash.to_s) puts("#{file_src} 上传完成!", :purple) end end desc 'upload_all [PATH] [FILE_SRC_PATH]', '上传目录下的所有文件(不含子目录)' method_option :biz_attr, aliases: '-r', desc: '业务属性' method_option :skip_error, aliases: '-k', desc: '跳过错误', type: :boolean, default: false method_option :min_slice_size, aliases: '-m', desc: '最小完整上传大小', type: :numeric, banner: 'bytes' method_option :auto_create_folder, aliases: '-f', desc: '自动创建目录', type: :boolean, default: false method_option :disable_cpt, aliases: '-d', desc: '禁用断点续传(分片上传时有效)', type: :boolean, default: false method_option :threads, aliases: '-t', desc: '线程数(分片上传时有效)', type: :numeric method_option :upload_retry, aliases: '-n', desc: '重试次数(分片上传时有效)', type: :numeric method_option :slice_size, aliases: '-s', desc: '分片上传时每个分片的大小(分片上传时有效)', type: :numeric method_option :cpt_file, aliases: '-e', desc: '指定断点续传记录(分片上传时有效)' def upload_all(path, file_src_path) rescue_errors do files = bucket.upload_all(path, file_src_path, enabled_options([:biz_attr, :skip_error, :min_slice_size, :auto_create_folder, :disable_cpt, :threads, :upload_retry, :slice_size, :cpt_file])) do |percent| puts("上传进度: #{(percent*100).round(2)}%", :green) end files.each do |file| puts(file.url, :blue) end puts("目录: #{file_src_path} 共#{files.count}个文件 上传完成!", :purple) end end desc 'download [PATH] [FILE_STORE]', '下载文件(大文件自动分片下载,支持多线程下载,断点续传)' method_option :min_slice_size, aliases: '-m', desc: '最小完整下载大小', type: :numeric, banner: 'bytes' method_option :disable_cpt, aliases: '-d', desc: '禁用断点续传(分片下载时有效)', type: :boolean, default: false method_option :threads, aliases: '-t', desc: '线程数(分片下载时有效)', type: :numeric method_option :download_retry, aliases: '-n', desc: '重试次数(分片下载时有效)', type: :numeric method_option :part_size, aliases: '-s', desc: '分片下载时每个分片的大小(分片下载时有效)', type: :numeric method_option :cpt_file, aliases: '-e', desc: '指定断点续传记录(分片下载时有效)' def download(path, file_store) rescue_errors do file = bucket.download(path, file_store, enabled_options([:min_slice_size, :disable_cpt, :threads, :download_retry, :part_size, :cpt_file])) do |percent| puts("下载进度: #{(percent*100).round(2)}%", :green) end puts(file, :blue) puts("#{file_store} 下载完成!", :purple) end end desc 'download_all [PATH] [FILE_STORE_PATH]', '下载目录下的所有文件(不含子目录)' method_option :min_slice_size, aliases: '-m', desc: '最小完整下载大小', type: :numeric, banner: 'bytes' method_option :disable_mkdir, aliases: '-k', desc: '禁止自动创建本地目录', type: :boolean, default: false method_option :disable_cpt, aliases: '-d', desc: '禁用断点续传(分片下载时有效)', type: :boolean, default: false method_option :threads, aliases: '-t', desc: '线程数(分片下载时有效)', type: :numeric method_option :download_retry, aliases: '-n', desc: '重试次数(分片下载时有效)', type: :numeric method_option :part_size, aliases: '-s', desc: '分片下载时每个分片的大小(分片下载时有效)', type: :numeric method_option :cpt_file, aliases: '-e', desc: '指定断点续传记录(分片下载时有效)' def download_all(path, file_store_path) rescue_errors do files = bucket.download_all(path, file_store_path, enabled_options([:min_slice_size, :disable_mkdir, :disable_cpt, :threads, :download_retry, :part_size, :cpt_file])) do |percent| puts("下载进度: #{(percent*100).round(2)}%", :green) end files.each do |file| puts(file, :blue) end puts("目录: #{path} 共#{files.count}个文件 下载完成!", :purple) end end desc 'update [PATH] [BIZ_ATTR]', '更新业务属性' def update(path, biz_attr) rescue_errors do bucket.update(path, biz_attr) puts("#{path} 更新成功!", :purple) end end desc 'delete [PATH]', '删除目录或文件' def delete(path) rescue_errors do bucket.delete(path) puts("#{path} 删除成功!", :purple) end end desc 'url [PATH]', '获取文件的访问URL' method_option :cname, aliases: '-e', desc: '使用CNAME', banner: 'cname.domain.com' method_option :https, aliases: '-s', desc: '使用HTTPS', type: :boolean method_option :expire_seconds, aliases: '-p', desc: '签名有效秒数(对私有空间有效)', type: :numeric def url(path) rescue_errors do puts(bucket.url(path, enabled_options([:cname, :https, :expire_seconds])), :blue) end end desc 'count [PATH]', '获取文件及目录数' def count(path) rescue_errors do puts(bucket.count(path), :blue) end end map size: :count desc 'count_files [PATH]', '获取文件数' def count_files(path) rescue_errors do puts(bucket.count_files(path), :blue) end end desc 'count_dirs [PATH]', '获取目录数' def count_dirs(path) rescue_errors do puts(bucket.count_dirs(path), :blue) end end desc 'is_exist [PATH]', '判断文件或目录是否存在' def is_exist(path) rescue_errors do puts(bucket.exist?(path), :blue) end end map exists: :exist desc 'is_empty [PATH]', '判断目录是否为空' def is_empty(path) rescue_errors do puts(bucket.empty?(path), :blue) end end desc 'is_complete [PATH]', '判断文件是否上传完整' def is_complete(path) rescue_errors do puts(bucket.complete?(path), :blue) end end desc 'sign_once [PATH]', '生成单次可用签名' def sign_once(path) rescue_errors do puts(bucket.client.signature.once(bucket.bucket_name, path), :blue) end end desc 'sign_multi [EXPIRE]', '生成多次可用签名' def sign_multi(expire) puts(bucket.client.signature.once(bucket.bucket_name, expire), :blue) end desc 'init', '创建默认配置文件' method_option :file, aliases: '-f', desc: '指定创建配置文件的路径', default: DEFAULT_CONFIG def init if File.exist?(File.expand_path(options[:file])) puts('文件已存在!', :red) exit! else yml = { 'app_id' => 'your_app_id', 'secret_id' => 'your_secret_id', 'secret_key' => 'your_secret_key', 'default_bucket' => 'your_default_bucket' } File.open(File.expand_path(options[:file]), 'w') do |f| f.write(yml.to_yaml) end puts("默认配置文件已创建在 #{options[:file]}", :purple) end end desc 'help [COMMAND]', '获取指令的使用帮助' def help(*args) super(*args) end private def enabled_options(hash_array) ep = Hash.new hash_array.each do |key| ep[key] = options[key] end ep end def format_puts_stat(stat) # 格式化hash字符串 stat.gsub!('{:', "{\n :") stat.gsub!('=>', ' => ') stat.gsub!(', :', ",\n :") stat.gsub!('}', "\n}") puts(stat, :blue) end def rescue_errors(&block) block.call if block rescue => error puts("#{error}", :red) exit! end def bucket # 判断配置文件是否存在 unless File.exist?(File.expand_path(options[:config])) puts('未找到配置文件, 使用 [cos init] 指令创建默认配置文件', :red) exit! end COS::Logging::set_logger(STDOUT, Logger::INFO) COS.client(config: options[:config]).bucket(options[:bucket]) end end # 打印输出着色 class Object def puts(message, color = nil) color_table = { :red => '31;1', :green => '32;1', :yellow => '33;1', :blue => '34;1', :purple => '35;1', :sky => '36;1', } if color and color_table.has_key?(color.to_sym) print "\e[#{color_table[color]}m" Kernel::puts message print "\e[0m" else Kernel::puts message end end end COS_CLI.start