├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── featute_report.md ├── pull_request_template.md └── workflows │ ├── release.yaml │ └── ut-check.yml ├── .gitignore ├── CHANGELOG.md ├── License ├── Makefile ├── OWNERS ├── README.md ├── cmd ├── ali.go ├── asyncfetch.go ├── autocompletion.go ├── aws.go ├── bucket.go ├── cdn.go ├── download.go ├── fop.go ├── m3u8.go ├── match.go ├── root.go ├── rs.go ├── rsbatch.go ├── servers.go ├── share.go ├── tools.go ├── upload.go ├── user.go └── version.go ├── cmd_test ├── aa_pre_test.go ├── ali_test.go ├── async_fetch_test.go ├── aws_test.go ├── bucket_domain_test.go ├── bucket_list_test.go ├── bucket_mk_test.go ├── bucket_test.go ├── cdn_prefetch_test.go ├── cdn_refresh_test.go ├── change_minetype_test.go ├── change_type_test.go ├── copy_test.go ├── delete_test.go ├── download_get_test.go ├── download_test.go ├── fetch_test.go ├── fop_test.go ├── forbidden_test.go ├── lifecycle_change_test.go ├── match_test.go ├── move_test.go ├── qshell_test.go ├── rename_test.go ├── restore_archive_test.go ├── servers_test.go ├── sign_test.go ├── status_test.go ├── test │ ├── config.go │ ├── document.go │ ├── file.go │ ├── flow.go │ ├── lineWriter.go │ └── test.go ├── tools_test.go ├── upload_form_test.go ├── upload_resume_test.go ├── upload_sync_test.go ├── upload_test.go ├── user.go ├── user_cmd_execute.go └── version_test.go ├── codecov.yml ├── docs ├── .DS_Store ├── abfetch.go ├── abfetch.md ├── account.go ├── account.md ├── acheck.go ├── acheck.md ├── alilistbucket.go ├── alilistbucket.md ├── awsfetch.go ├── awsfetch.md ├── awslist.go ├── awslist.md ├── b64decode.go ├── b64decode.md ├── b64encode.go ├── b64encode.md ├── batchchgm.go ├── batchchgm.md ├── batchchlifecycle.go ├── batchchlifecycle.md ├── batchchtype.go ├── batchchtype.md ├── batchcopy.go ├── batchcopy.md ├── batchdelete.go ├── batchdelete.md ├── batchexpire.go ├── batchexpire.md ├── batchfetch.go ├── batchfetch.md ├── batchforbidden.go ├── batchforbidden.md ├── batchmatch.go ├── batchmatch.md ├── batchmove.go ├── batchmove.md ├── batchrename.go ├── batchrename.md ├── batchrestorear.go ├── batchrestorear.md ├── batchsign.go ├── batchsign.md ├── batchstat.go ├── batchstat.md ├── bucket.go ├── bucket.md ├── buckets.go ├── buckets.md ├── cdnprefetch.go ├── cdnprefetch.md ├── cdnrefresh.go ├── cdnrefresh.md ├── chgm.go ├── chgm.md ├── chlifecycle.go ├── chlifecycle.md ├── chtype.go ├── chtype.md ├── copy.go ├── copy.md ├── create-share.go ├── create-share.md ├── d2ts.go ├── d2ts.md ├── delete.go ├── delete.md ├── dircache.go ├── dircache.md ├── document.go ├── domains.go ├── domains.md ├── expire.go ├── expire.md ├── fetch.go ├── fetch.md ├── forbidden.go ├── forbidden.md ├── fput.go ├── fput.md ├── func.go ├── func.md ├── get.go ├── get.md ├── ip.go ├── ip.md ├── listbucket.go ├── listbucket.md ├── listbucket2.go ├── listbucket2.md ├── m3u8delete.go ├── m3u8delete.md ├── m3u8replace.go ├── m3u8replace.md ├── match.go ├── match.md ├── mirrorupdate.go ├── mirrorupdate.md ├── mkbucket.go ├── mkbucket.md ├── move.go ├── move.md ├── pfop.go ├── pfop.md ├── prefetch.go ├── prefetch.md ├── prefop.go ├── prefop.md ├── privateurl.go ├── privateurl.md ├── qdownload.go ├── qdownload.md ├── qdownload2.go ├── qdownload2.md ├── qetag.go ├── qetag.md ├── qshell.go ├── qupload.go ├── qupload.md ├── qupload2.go ├── qupload2.md ├── rename.go ├── rename.md ├── reqid.go ├── reqid.md ├── restorear.go ├── restorear.md ├── rpcdecode.go ├── rpcdecode.md ├── rpcencode.go ├── rpcencode.md ├── rput.go ├── rput.md ├── saveas.go ├── saveas.md ├── share-cp.go ├── share-cp.md ├── share-ls.go ├── share-ls.md ├── stat.go ├── stat.md ├── sync.go ├── sync.md ├── tms2d.go ├── tms2d.md ├── tns2d.go ├── tns2d.md ├── token.go ├── token.md ├── ts2d.go ├── ts2d.md ├── unzip.go ├── unzip.md ├── urldecode.go ├── urldecode.md ├── urlencode.go ├── urlencode.md ├── user.go ├── user.md ├── version.go └── version.md ├── go.mod ├── go.sum ├── iqshell ├── ali │ └── list_bucket.go ├── aws │ ├── fetch.go │ └── list_bucket.go ├── cdn │ ├── cdn.go │ └── operations │ │ ├── prefetch.go │ │ ├── qps.go │ │ └── refresh.go ├── common │ ├── .DS_Store │ ├── account │ │ ├── account.go │ │ ├── actions.go │ │ ├── crypt.go │ │ ├── load.go │ │ └── operations │ │ │ ├── add.go │ │ │ ├── delete.go │ │ │ ├── query.go │ │ │ └── update.go │ ├── alert │ │ └── alert.go │ ├── client │ │ └── client.go │ ├── config │ │ ├── config.go │ │ ├── config_global.go │ │ ├── config_local.go │ │ ├── config_user.go │ │ ├── hosts.go │ │ ├── load.go │ │ ├── log.go │ │ ├── operation.go │ │ ├── retry.go │ │ ├── tasks.go │ │ └── viper.go │ ├── data │ │ ├── base.go │ │ ├── checker.go │ │ ├── data_test.go │ │ ├── define.go │ │ ├── env.go │ │ ├── error.go │ │ ├── global_var.go │ │ └── std.go │ ├── db │ │ └── db.go │ ├── export │ │ ├── export.go │ │ └── file.go │ ├── file │ │ ├── rotate_file.go │ │ └── rotate_file_test.go │ ├── flow │ │ ├── builder.go │ │ ├── code_verification.go │ │ ├── event_listener.go │ │ ├── flow.go │ │ ├── overseer.go │ │ ├── overseer_local_db.go │ │ ├── redo.go │ │ ├── result.go │ │ ├── skip.go │ │ ├── work.go │ │ ├── work_creator.go │ │ ├── work_creator_items.go │ │ ├── work_creator_json.go │ │ ├── work_limit.go │ │ ├── work_provider.go │ │ ├── work_provider_array.go │ │ ├── work_provider_chan.go │ │ ├── work_provider_file.go │ │ ├── work_provider_reader.go │ │ ├── worker.go │ │ └── worker_provider.go │ ├── host │ │ ├── host.go │ │ └── provider.go │ ├── limit │ │ ├── block.go │ │ └── limit.go │ ├── locker │ │ └── locker.go │ ├── log │ │ ├── console.go │ │ ├── data.go │ │ ├── load.go │ │ └── log.go │ ├── progress │ │ ├── printer.go │ │ └── progress.go │ ├── provider │ │ ├── data.go │ │ └── provider.go │ ├── recorder │ │ ├── db.go │ │ └── recorder.go │ ├── scanner │ │ └── scanner.go │ ├── synchronized │ │ └── sync.go │ ├── utils │ │ ├── cmd.go │ │ ├── cmd_test.go │ │ ├── commits.go │ │ ├── commits_test.go │ │ ├── crypto.go │ │ ├── dir_cache.go │ │ ├── empty.go │ │ ├── error.go │ │ ├── etag.go │ │ ├── file.go │ │ ├── ip.go │ │ ├── ip │ │ │ ├── group.go │ │ │ ├── parser.go │ │ │ └── parser_ali.go │ │ ├── ip_test.go │ │ ├── math.go2 │ │ ├── operations │ │ │ ├── base64.go │ │ │ ├── dir_cache.go │ │ │ ├── etag.go │ │ │ ├── func.go │ │ │ ├── ip.go │ │ │ ├── reqid.go │ │ │ ├── rpc.go │ │ │ ├── timestamp.go │ │ │ ├── token.go │ │ │ ├── url.go │ │ │ └── zip.go │ │ ├── os.go │ │ ├── path.go │ │ ├── strings.go │ │ ├── template.go │ │ ├── unzip.go │ │ ├── user_agent.go │ │ ├── utils.go │ │ └── utils_test.go │ ├── version │ │ ├── operations │ │ │ └── version.go │ │ └── version.go │ └── workspace │ │ ├── config.go │ │ ├── interrupt.go │ │ ├── load.go │ │ ├── path.go │ │ └── workspace.go ├── load.go └── storage │ ├── .DS_Store │ ├── bucket │ ├── bucket.go │ ├── create.go │ ├── domain.go │ ├── internal │ │ └── list │ │ │ ├── list.go │ │ │ ├── list_v1.go │ │ │ └── list_v2.go │ ├── list.go │ ├── list_cache.go │ ├── list_line.go │ ├── operations │ │ ├── bucket.go │ │ ├── create.go │ │ ├── domain_list.go │ │ └── list.go │ ├── region.go │ └── storage_v2.go │ ├── object │ ├── .DS_Store │ ├── batch │ │ ├── batch.go │ │ ├── data.go │ │ ├── metric.go │ │ ├── one.go │ │ └── some.go │ ├── copy.go │ ├── delete.go │ ├── download │ │ ├── downloader.go │ │ ├── downloader_file.go │ │ ├── file_info.go │ │ ├── operations │ │ │ ├── batch.go │ │ │ ├── batch_data.go │ │ │ ├── download.go │ │ │ ├── download_host.go │ │ │ ├── metric.go │ │ │ ├── utils.go │ │ │ └── work_provider.go │ │ ├── slice_downloader.go │ │ └── url.go │ ├── exist.go │ ├── fetch.go │ ├── fop.go │ ├── lifecycle.go │ ├── m3u8 │ │ ├── delete.go │ │ ├── m3u8_test.go │ │ ├── operations │ │ │ ├── delete.go │ │ │ └── replace.go │ │ ├── replace.go │ │ └── slices.go │ ├── match.go │ ├── mime_change.go │ ├── move.go │ ├── operations │ │ ├── copy.go │ │ ├── delete.go │ │ ├── fetch.go │ │ ├── fetch_async.go │ │ ├── fop.go │ │ ├── lifecycle.go │ │ ├── match.go │ │ ├── mime_change.go │ │ ├── move.go │ │ ├── prefetch.go │ │ ├── private_url.go │ │ ├── rename.go │ │ ├── restore_archive.go │ │ ├── save_as.go │ │ ├── share.go │ │ ├── status.go │ │ ├── status_change.go │ │ └── type_change.go │ ├── restore_archive.go │ ├── save_as.go │ ├── status.go │ ├── type_change.go │ └── upload │ │ ├── api │ │ ├── recorder.go │ │ ├── resume.go │ │ ├── resume_v1.go │ │ └── resume_v2.go │ │ ├── conveyor.go │ │ ├── operations │ │ ├── batch.go │ │ ├── batch_data.go │ │ ├── config_mould.go │ │ ├── metric.go │ │ ├── sync.go │ │ ├── upload.go │ │ └── utils.go │ │ ├── uploader.go │ │ ├── uploader_form.go │ │ ├── uploader_resume_v1.go │ │ └── uploader_resume_v2.go │ ├── prefetch.go │ └── servers │ ├── all_buckets.go │ └── operations │ └── bucket_list.go ├── main └── main.go └── readme.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '缺陷问题反馈' 3 | about: '如何提出一个 Issue' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### 问题描述 11 | 12 | 13 | 14 | #### 重现步骤 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 20 | 21 | #### 期望结果? 22 | 23 | - 24 | 25 | 26 | #### 实际结果? 27 | 28 | - 29 | 30 | 31 | #### 错误日志/截图/配置参数 32 | 33 | 34 | 35 | 36 | 37 | #### 相关环境信息 38 | - **操作系统**: 39 | - **qshell版本**: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/featute_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '新需求' 3 | about: '关于项目的request feature' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### 需求描述 11 | 12 | 13 | 14 | 15 | #### 其他相关建议 16 | 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | #### 变更背景描述 2 | 3 | 4 | #### jira issue链接 5 | 6 | 7 | #### 主要变更点 8 | - [ ] fix bug 9 | - [ ] new feature 10 | - [ ] 不兼容变更 11 | 12 | 13 | #### Checklist 14 | - [ ] 已自测 15 | - [ ] 已更新Readme 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/ut-check.yml: -------------------------------------------------------------------------------- 1 | name: ut-check 2 | on: [push, pull_request] 3 | jobs: 4 | run: 5 | name: go test 6 | strategy: 7 | matrix: 8 | go-version: [1.23.x] 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | # This step checks out a copy of your repository. 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | - name: Check format 19 | run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi 20 | - name: Go test 21 | run: | 22 | go test -coverprofile=coverage.txt ./... 23 | bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.dylib 9 | src/test/ 10 | src/.qshell 11 | marker.go 12 | marker_test.go 13 | build.sh 14 | build_local.sh 15 | 16 | # Folders 17 | _obj 18 | _test 19 | bin 20 | .DS_Store 21 | .idea 22 | .vscode 23 | 24 | # Architecture specific extensions/prefixes 25 | *.cgo1.go 26 | *.cgo2.c 27 | *.prof 28 | _cgo_defun.c 29 | _cgo_gotypes.go 30 | _cgo_export.* 31 | 32 | *.[568vq] 33 | [568vq].out 34 | report.xml 35 | .qshell 36 | qshell-* 37 | _testmain.go 38 | 39 | # go test coverage file 40 | coverage.txt 41 | 42 | # Test binary, built with `go test -c` 43 | *.test 44 | 45 | # Output of the go coverage tool, specifically when used with LiteIDE 46 | *.out 47 | 48 | # Dependency directories (remove the comment below to include it) 49 | # vendor/ 50 | 51 | # Go workspace file 52 | go.work 53 | 54 | ### Go Patch ### 55 | /vendor/ 56 | /Godeps/ 57 | 58 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Qiniu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | # see the OWNERS docs at https://go.k8s.io/owners 2 | 3 | approvers: 4 | - CarlJi 5 | - bachue 6 | - YangSen-qn 7 | - Mei-Zhao 8 | - sxci 9 | reviewers: 10 | - CarlJi 11 | - bachue 12 | - YangSen-qn 13 | - Mei-Zhao 14 | - sxci 15 | -------------------------------------------------------------------------------- /cmd/ali.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/docs" 5 | "github.com/qiniu/qshell/v2/iqshell" 6 | "github.com/qiniu/qshell/v2/iqshell/ali" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var aliListBucketCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { 11 | var info = ali.ListBucketInfo{} 12 | var cmd = &cobra.Command{ 13 | Use: "alilistbucket [Prefix] ", 14 | Short: "List all the file in the bucket of aliyun oss by prefix", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | cfg.CmdCfg.CmdId = docs.AliListBucket 17 | if len(args) > 0 { 18 | info.DataCenter = args[0] 19 | } 20 | if len(args) > 1 { 21 | info.Bucket = args[1] 22 | } 23 | if len(args) > 2 { 24 | info.AccessKey = args[2] 25 | } 26 | if len(args) > 3 { 27 | info.SecretKey = args[3] 28 | } 29 | if len(args) > 4 { 30 | if len(args) == 6 { 31 | info.Prefix = args[4] 32 | info.SaveToFile = args[5] 33 | } else { 34 | info.SaveToFile = args[4] 35 | } 36 | } 37 | ali.ListBucket(cfg, info) 38 | }, 39 | } 40 | return cmd 41 | } 42 | 43 | func init() { 44 | registerLoader(aliCmdLoader) 45 | } 46 | 47 | func aliCmdLoader(superCmd *cobra.Command, cfg *iqshell.Config) { 48 | superCmd.AddCommand(aliListBucketCmdBuilder(cfg)) 49 | } 50 | -------------------------------------------------------------------------------- /cmd/servers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/docs" 5 | "github.com/qiniu/qshell/v2/iqshell" 6 | "github.com/qiniu/qshell/v2/iqshell/storage/servers/operations" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var bucketsCmdBuilder = func(cfg *iqshell.Config) *cobra.Command { 11 | var info = operations.ListInfo{} 12 | var cmd = &cobra.Command{ 13 | Use: "buckets", 14 | Short: "Get all buckets of the account", 15 | Args: cobra.ExactArgs(0), 16 | Run: func(cmd *cobra.Command, args []string) { 17 | cfg.CmdCfg.CmdId = docs.BucketsType 18 | operations.List(cfg, info) 19 | }, 20 | } 21 | cmd.Flags().StringVarP(&info.Region, "region", "", "", "region of bucket; z0, z1, z2, as0, na0 etc") 22 | cmd.Flags().BoolVarP(&info.Detail, "detail", "", false, "print detail info for bucket") 23 | return cmd 24 | } 25 | 26 | func init() { 27 | registerLoader(serversCmdLoader) 28 | } 29 | 30 | func serversCmdLoader(superCmd *cobra.Command, cfg *iqshell.Config) { 31 | superCmd.AddCommand( 32 | bucketsCmdBuilder(cfg), 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/docs" 5 | "github.com/qiniu/qshell/v2/iqshell" 6 | "github.com/qiniu/qshell/v2/iqshell/common/version/operations" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func versionCmdBuilder(cfg *iqshell.Config) *cobra.Command { 11 | var cmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Show version", 14 | Run: func(cmd *cobra.Command, params []string) { 15 | cfg.CmdCfg.CmdId = docs.VersionType 16 | operations.Version(cfg, operations.VersionInfo{}) 17 | }, 18 | } 19 | return cmd 20 | } 21 | 22 | func init() { 23 | registerLoader(versionCmdLoader) 24 | } 25 | 26 | func versionCmdLoader(superCmd *cobra.Command, cfg *iqshell.Config) { 27 | superCmd.AddCommand( 28 | versionCmdBuilder(cfg), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /cmd_test/aa_pre_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/qiniu/qshell/v2/cmd_test/test" 10 | "github.com/qiniu/qshell/v2/iqshell/common/data" 11 | ) 12 | 13 | func TestCmd(t *testing.T) { 14 | data.SetTestMode() 15 | 16 | TestUser(t) 17 | ClearCache(t) 18 | } 19 | 20 | func ClearCache(t *testing.T) { 21 | path := test.RemoveRootPath() 22 | if err := path; err != nil { 23 | fmt.Printf("Remove Cache Path:%s error:%v", path, err) 24 | } 25 | 26 | path = test.RemoveTempPath() 27 | if err := path; err != nil { 28 | fmt.Printf("Remove Cache Path:%s error:%v", path, err) 29 | } 30 | 31 | path = test.RemoveResultPath() 32 | if err := path; err != nil { 33 | fmt.Printf("Remove Cache Path:%s error:%v", path, err) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cmd_test/ali_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestAliBucketListNoDataCenter(t *testing.T) { 12 | _, errs := test.RunCmdWithError("alilistbucket") 13 | if !strings.Contains(errs, "DataCenter can't be empty") { 14 | t.Fatal("empty DataCenter check error") 15 | } 16 | return 17 | } 18 | 19 | func TestAliBucketListNoBucket(t *testing.T) { 20 | _, errs := test.RunCmdWithError("alilistbucket", "DataCenter") 21 | if !strings.Contains(errs, "Bucket can't be empty") { 22 | t.Fatal("empty Bucket check error") 23 | } 24 | return 25 | } 26 | 27 | func TestAliBucketListNoAccessKeyId(t *testing.T) { 28 | _, errs := test.RunCmdWithError("alilistbucket", "DataCenter", "Bucket") 29 | if !strings.Contains(errs, "AccessKeyId can't be empty") { 30 | t.Fatal("empty AccessKeyId check error") 31 | } 32 | 33 | return 34 | } 35 | 36 | func TestAliBucketListNoAccessKeySecret(t *testing.T) { 37 | _, errs := test.RunCmdWithError("alilistbucket", "DataCenter", "Bucket", "AccessKeyId") 38 | if !strings.Contains(errs, "AccessKeySecret can't be empty") { 39 | t.Fatal("empty AccessKeySecret check error") 40 | } 41 | return 42 | } 43 | 44 | func TestAliBucketListDocument(t *testing.T) { 45 | test.TestDocument("alilistbucket", t) 46 | } 47 | -------------------------------------------------------------------------------- /cmd_test/bucket_domain_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestBucketDomain(t *testing.T) { 12 | result, errs := test.RunCmdWithError("domains", test.Bucket) 13 | if len(errs) > 0 { 14 | t.Fatal("error:", errs) 15 | } 16 | 17 | if !strings.Contains(result, test.BucketDomain) { 18 | t.Fatal("no expected domain:%", test.BucketDomain) 19 | } 20 | 21 | return 22 | } 23 | 24 | func TestBucketDomainNoBucket(t *testing.T) { 25 | _, err := test.RunCmdWithError("domains") 26 | if !strings.Contains(err, "Bucket can't be empty") { 27 | t.Fail() 28 | } 29 | } 30 | 31 | func TestBucketDomainDocument(t *testing.T) { 32 | test.TestDocument("domains", t) 33 | } 34 | -------------------------------------------------------------------------------- /cmd_test/bucket_mk_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestMkBucket(t *testing.T) { 12 | 13 | _, errs := test.RunCmdWithError("mkbucket", test.Bucket, "--region", "z1", "--private") 14 | if len(errs) == 0 { 15 | t.Fatal("should return bucket exists") 16 | } 17 | 18 | if !strings.Contains(errs, "error:the bucket already exists") { 19 | t.Fatal("expected error:bucket exists, but:" + errs) 20 | } 21 | 22 | return 23 | } 24 | 25 | func TestMkBucketNotExistRegion(t *testing.T) { 26 | 27 | _, errs := test.RunCmdWithError("mkbucket", test.Bucket, "--region", "z10", "--private") 28 | if len(errs) == 0 { 29 | t.Fatal("should return bucket exists") 30 | } 31 | 32 | if !strings.Contains(errs, "error:invalid region parameter") { 33 | t.Fatal("expected error:error:invalid region parameter, but:" + errs) 34 | } 35 | 36 | return 37 | } 38 | 39 | func TestMkBucketNoBucket(t *testing.T) { 40 | _, err := test.RunCmdWithError("mkbucket") 41 | if !strings.Contains(err, "Bucket can't be empty") { 42 | t.Fail() 43 | } 44 | } 45 | 46 | func TestMkBucketDocument(t *testing.T) { 47 | test.TestDocument("mkbucket", t) 48 | } 49 | -------------------------------------------------------------------------------- /cmd_test/bucket_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestBucket(t *testing.T) { 12 | 13 | ret, errs := test.RunCmdWithError("bucket", test.Bucket) 14 | if len(errs) > 0 { 15 | t.Fatal("get bucket info error:" + errs) 16 | } 17 | 18 | if !strings.Contains(ret, test.Bucket) { 19 | t.Fatal("UnExcepted bucket info:" + ret) 20 | } 21 | 22 | return 23 | } 24 | 25 | func TestBucketNoBucket(t *testing.T) { 26 | _, err := test.RunCmdWithError("bucket") 27 | if !strings.Contains(err, "Bucket can't be empty") { 28 | t.Fail() 29 | } 30 | } 31 | 32 | func TestBucketDocument(t *testing.T) { 33 | test.TestDocument("bucket", t) 34 | } 35 | -------------------------------------------------------------------------------- /cmd_test/cdn_prefetch_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestCdnPrefetch(t *testing.T) { 12 | path, err := test.CreateFileWithContent("cdn_prefetch.txt", test.BucketObjectDomainsString) 13 | if err != nil { 14 | t.Fatal("create cdn config file error:", err) 15 | } 16 | 17 | result, errString := test.RunCmdWithError("cdnprefetch", "-i", path, "--qps", "1", "--size", "2", "-D") 18 | if !strings.Contains(result, "CDN prefetch Code: 200, FlowInfo: success") && 19 | !strings.Contains(errString, "count limit error") { 20 | t.Fail() 21 | } 22 | 23 | test.RemoveFile(path) 24 | 25 | return 26 | } 27 | 28 | func TestCdnPrefetchDocument(t *testing.T) { 29 | test.TestDocument("cdnprefetch", t) 30 | } 31 | -------------------------------------------------------------------------------- /cmd_test/cdn_refresh_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestCdnRefreshFile(t *testing.T) { 12 | path, err := test.CreateFileWithContent("cdn_refresh.txt", test.BucketObjectDomainsString) 13 | if err != nil { 14 | t.Fatal("create cdn config file error:", err) 15 | } 16 | 17 | result, errString := test.RunCmdWithError("cdnrefresh", "-i", path, "--qps", "1", "--size", "2", "-d") 18 | if !strings.Contains(result, "CDN refresh Code: 200, FlowInfo: success") && 19 | !strings.Contains(errString, "count limit error") { 20 | t.Fail() 21 | } 22 | 23 | test.RemoveFile(path) 24 | 25 | return 26 | } 27 | 28 | func TestCdnRefreshDirs(t *testing.T) { 29 | path, err := test.CreateFileWithContent("cdn_refresh.txt", test.BucketObjectDomainsString) 30 | if err != nil { 31 | t.Fatal("create cdn config file error:", err) 32 | } 33 | 34 | _, errString := test.RunCmdWithError("cdnrefresh", "--dirs", "-i", path, "--qps", "1", "--size", "2") 35 | if len(errString) > 0 && !strings.Contains(errString, "count limit error") { 36 | t.Fail() 37 | } 38 | 39 | test.RemoveFile(path) 40 | 41 | return 42 | } 43 | 44 | func TestCdnRefreshDocument(t *testing.T) { 45 | test.TestDocument("cdnrefresh", t) 46 | } 47 | -------------------------------------------------------------------------------- /cmd_test/qshell_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/cmd_test/test" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestQShellDocument01(t *testing.T) { 10 | prefix := "# qshell" 11 | result, _ := test.RunCmdWithError() 12 | if !strings.HasPrefix(result, prefix) { 13 | t.Fatal("document test fail for cmd: qshell") 14 | } 15 | } 16 | 17 | func TestQShellDocument02(t *testing.T) { 18 | prefix := "# qshell" 19 | result, _ := test.RunCmdWithError("--doc") 20 | if !strings.HasPrefix(result, prefix) { 21 | t.Fatal("document test fail for cmd: qshell") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cmd_test/servers_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/qiniu/qshell/v2/cmd_test/test" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestBuckets(t *testing.T) { 12 | result, errs := test.RunCmdWithError("buckets") 13 | if len(errs) > 0 { 14 | t.Fatal("error:", errs) 15 | } 16 | 17 | if !strings.Contains(result, test.Bucket) { 18 | t.Fatal("no expected bucket:%", test.Bucket) 19 | } 20 | return 21 | } 22 | 23 | func TestBucketsDocument(t *testing.T) { 24 | test.TestDocument("buckets", t) 25 | } 26 | -------------------------------------------------------------------------------- /cmd_test/sign_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package cmd 4 | 5 | import ( 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/qiniu/qshell/v2/cmd_test/test" 10 | ) 11 | 12 | func TestBatchSign(t *testing.T) { 13 | path, err := test.CreateFileWithContent("batch_sign.txt", test.BucketObjectDomainsString) 14 | if err != nil { 15 | t.Fatal("create batch sign config file error:", err) 16 | } 17 | 18 | resultDir, err := test.ResultPath() 19 | if err != nil { 20 | t.Fatal("get result dir error:", err) 21 | } 22 | 23 | resultLogPath := filepath.Join(resultDir, "batch_result.txt") 24 | 25 | result, errs := test.RunCmdWithError("batchsign", 26 | "-i", path, 27 | "--outfile", resultLogPath, 28 | ) 29 | if len(errs) > 0 { 30 | t.Fail() 31 | } 32 | 33 | if len(result) == 0 { 34 | t.Fail() 35 | } 36 | 37 | defer func() { 38 | test.RemoveFile(resultLogPath) 39 | }() 40 | 41 | if !test.IsFileHasContent(resultLogPath) { 42 | t.Fatal("batch result: output to file error: file empty") 43 | } 44 | } 45 | 46 | func TestBatchSignDocument(t *testing.T) { 47 | test.TestDocument("batchsign", t) 48 | } 49 | -------------------------------------------------------------------------------- /cmd_test/test/document.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestDocument(cmdName string, t *testing.T) { 10 | prefix := fmt.Sprintf("# 简介\n`%s`", cmdName) 11 | result, _ := RunCmdWithError(cmdName, DocumentOption) 12 | if !strings.HasPrefix(result, prefix) { 13 | t.Fatal("document test fail for cmd:" + cmdName) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd_test/test/lineWriter.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | ) 8 | 9 | type lineWriter struct { 10 | mu sync.Mutex 11 | buff string 12 | WriteStringFunc func(line string) 13 | } 14 | 15 | func newLineWriter(writeStringFunc func(line string)) *lineWriter { 16 | lw := &lineWriter{ 17 | buff: "", 18 | WriteStringFunc: writeStringFunc, 19 | } 20 | return lw 21 | } 22 | 23 | func (w *lineWriter) Write(p []byte) (n int, err error) { 24 | w.mu.Lock() 25 | defer w.mu.Unlock() 26 | 27 | w.buff += string(p) 28 | for len(w.buff) > 0 { 29 | items := strings.SplitN(w.buff, "\n", 2) 30 | if len(items) < 2 { 31 | break 32 | } else { 33 | line := items[0] 34 | fmt.Printf("line:%s\n", line) 35 | w.buff = items[1] 36 | if w.WriteStringFunc != nil { 37 | w.WriteStringFunc(line + "\n") 38 | } 39 | } 40 | } 41 | 42 | return len(p), nil 43 | } 44 | 45 | func (w *lineWriter) Close() error { 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd_test/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func RunCmd(t *testing.T, args ...string) string { 8 | result := "" 9 | NewTestFlow(args...).ResultHandler(func(line string) { 10 | result += line 11 | }).ErrorHandler(DefaultTestErrorHandler(t)).Run() 12 | return result 13 | } 14 | 15 | func RunCmdWithError(args ...string) (string, string) { 16 | result := "" 17 | err := "" 18 | NewTestFlow(args...).ResultHandler(func(line string) { 19 | result += line 20 | }).ErrorHandler(func(line string) { 21 | err += line 22 | }).Run() 23 | return result, err 24 | } 25 | 26 | func DefaultTestErrorHandler(t *testing.T) func(line string) { 27 | return func(line string) { 28 | t.Fail() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd_test/version_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package cmd 4 | 5 | import ( 6 | "strings" 7 | "testing" 8 | 9 | "github.com/qiniu/qshell/v2/cmd_test/test" 10 | ) 11 | 12 | func TestVersion(t *testing.T) { 13 | result := test.RunCmd(t, "version") 14 | if !strings.Contains(result, "UNSTABLE") { 15 | t.Fatal("version") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | ci: 3 | - prow.qiniu.io # prow 里面运行需添加,其他 CI 不要 4 | require_ci_to_pass: no # 改为 no,否则 codecov 会等待其他 GitHub 上所有 CI 通过才会留言。 5 | 6 | github_checks: #关闭github checks 7 | annotations: false 8 | 9 | comment: 10 | layout: "reach, diff, flags, files" 11 | behavior: new # 默认是更新旧留言,改为 new,删除旧的,增加新的。 12 | require_changes: false # if true: only post the comment if coverage changes 13 | require_base: no # [yes :: must have a base report to post] 14 | require_head: yes # [yes :: must have a head report to post] 15 | branches: # branch names that can post comment 16 | - "master" 17 | 18 | coverage: 19 | status: # 评判 pr 通过的标准 20 | patch: off 21 | project: # project 统计所有代码x 22 | default: 23 | # basic 24 | target: 11% # 总体通过标准 25 | threshold: 3% # 允许单次下降的幅度 26 | base: auto 27 | if_not_found: success 28 | if_ci_failed: error 29 | -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/qshell/b0b048646b3a18eab15c89c5c4f453c4a2f10c30/docs/.DS_Store -------------------------------------------------------------------------------- /docs/abfetch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed abfetch.md 6 | var abFetchDocument string 7 | 8 | const ABFetch = "abfetch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ABFetch, abFetchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/account.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed account.md 6 | var accountDocument string 7 | 8 | const Account = "account" 9 | 10 | func init() { 11 | addCmdDocumentInfo(Account, accountDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/acheck.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed acheck.md 6 | var aCheckDocument string 7 | 8 | const ACheckType = "acheck" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ACheckType, aCheckDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/acheck.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `acheck` 查询异步抓取任务状态。 3 | 4 | 参考文档:[查询状态 (async fetch)](https://developer.qiniu.com/kodo/api/4097/asynch-fetch) 5 | 6 | # 格式 7 | ``` 8 | qshell acheck [flags] 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell acheck -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell acheck --doc 19 | ``` 20 | 21 | # 鉴权 22 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 23 | 24 | # 参数 25 | - Bucket:空间名,可以为公开空间或私有空间。 【必选】 26 | - ID:异步 fetch 返回的任务 ID。 【必选】 27 | 28 | 详细的选项介绍,请参考:[查询状态 (async fetch)](https://developer.qiniu.com/kodo/api/4097/asynch-fetch) 29 | 30 | # 示例 31 | 异步 fetch 返回 ID `eyJ6b25lIjoibmEwIiwicXVldWUiOiJTSVNZUEhVUy1KT0JTLVYzIiwicGFydF9pZCI6OSwib2Zmc2V0Ijo1NTEzMTU3fQ==`, bucket 名为 test: 32 | ``` 33 | qshell acheck test eyJ6b25lIjoibmEwIiwicXVldWUiOiJTSVNZUEhVUy1KT0JTLVYzIiwicGFydF9pZCI6OSwib2Zmc2V0Ijo1NTEzMTU3fQ== 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /docs/alilistbucket.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed alilistbucket.md 6 | var aliListBucketDocument string 7 | 8 | const AliListBucket = "alilistbucket" 9 | 10 | func init() { 11 | addCmdDocumentInfo(AliListBucket, aliListBucketDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/alilistbucket.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `alilistbucket` 用来列举阿里云 OSS 空间中的文件列表。 3 | 4 | # 格式 5 | ``` 6 | qshell alilistbucket [Prefix] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell alilistbucket -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell alilistbucket --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - DataCenter:阿里云 OSS 空间所在的数据中心域名。【必选】 24 | - Bucket:阿里云 OSS 空间名称,可以为公开空间或私有空间。【必选】 25 | - AccessKeyId:阿里云账号对应的 AccessKeyId [获取](https://ak-console.aliyun.com/#/accesskey) 。【必选】 26 | - AccessKeySecret:阿里云账号对应的 AccessKeySecret [获取](https://ak-console.aliyun.com/#/accesskey) 。【必选】 27 | - Prefix:阿里云 OSS 空间中文件的前缀。【可选】 28 | - ListBucketResultFile:文件列表保存的文件名称,可以为绝对路径或者相对路径。【必选】 29 | 30 | #### 阿里 OSS 公网数据中心域名 31 | - 杭州:oss-cn-hangzhou.aliyuncs.com 32 | - 青岛:oss-cn-qingdao.aliyuncs.com 33 | - 香港:oss-cn-hongkong.aliyuncs.com 34 | - 北京:oss-cn-beijing.aliyuncs.com 35 | - 深圳:oss-cn-shenzhen.aliyuncs.com 36 | 37 | #### 阿里 OSS 内网数据中心域名 38 | - 杭州:oss-cn-hangzhou-internal.aliyuncs.com 39 | - 青岛:oss-cn-qingdao-internal.aliyuncs.com 40 | - 香港:oss-cn-hongkong-internal.aliyuncs.com 41 | - 北京:oss-cn-beijing-internal.aliyuncs.com 42 | - 深圳:oss-cn-shenzhen-internal.aliyuncs.com 43 | 44 | # 示例 45 | 1.获取阿里云 OSS 空间 `qdisk-hz` 里面的所有文件列表: 46 | ``` 47 | qshell alilistbucket oss-cn-hangzhou.aliyuncs.com qdisk-hz poeDElTwLc2w0iFJ pPlaT3umFa1lcXTwp7N5nVQt9av1yg qdisk-hz.list.txt 48 | ``` 49 | 50 | 2.获取阿里云 OSS 空间 `qdisk-hz` 里面的以 `2015/01/18` 为前缀的文件列表: 51 | ``` 52 | qshell alilistbucket oss-cn-hangzhou.aliyuncs.com qdisk-hz poeDElTwLc2w0iFJ pPlaT3umFa1lcXTwp7N5nVQt9av1yg "2015/01/18" qdisk-hz.prefix.list.txt 53 | ``` 54 | 55 | 获取的文件内容组织方式为: 56 | ``` 57 | Key\tSize\tPutTime 58 | ``` 59 | 60 | 比如: 61 | ``` 62 | bucket_domain.png 287727 14215611840000000 63 | ``` -------------------------------------------------------------------------------- /docs/awsfetch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed awsfetch.md 6 | var awsFetchDocument string 7 | 8 | const AwsFetch = "awsfetch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(AwsFetch, awsFetchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/awslist.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed awslist.md 6 | var awsListDocument string 7 | 8 | const AwsList = "awslist" 9 | 10 | func init() { 11 | addCmdDocumentInfo(AwsList, awsListDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/awslist.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `awslist` 使用亚马逊的 List Objects V2 接口[文档](https://docs.aws.amazon.com/AmazonS3/latest/API/v2-RESTBucketGET.html), 获取空间中的文件, 打印到标准输出。 3 | 4 | 该命令的数据格式为: 5 | ``` 6 | <文件名>\t<文件大小>\t\t<最后修改时间> //注: \t 为 Tab 键 7 | ``` 8 | 9 | 当程序列举的过程中遇到错误,比如网络断开等, 会把当前的 ContinuationToken 打印到标准错误输出上, 可以使用 shell 重定向把标准输出到一个文件, 这样可以方便地找到 continuationToken 继续列举。 10 | 11 | # 格式 12 | ``` 13 | qshell awslist [-p ][-n ][-m ] -S -A 14 | ``` 15 | 16 | # 帮助文档 17 | 可以在命令行输入如下命令获取帮助文档: 18 | ``` 19 | // 简单描述 20 | $ qshell awslist -h 21 | 22 | // 详细文档(此文档) 23 | $ qshell awslist --doc 24 | ``` 25 | 26 | # 鉴权 27 | 无 28 | 29 | # 参数 30 | - AwsBucket: 亚马逊存储空间名称 31 | - AwsRegion: 亚马逊存储空间所在的地区 32 | 33 | # 选项 34 | - -A/--aws-id:亚马逊账户的 Access Key ID 。【必选】 35 | - -S/--aws-secret-key:亚马逊账户的 Secret Key 。【必选】 36 | - -p/--prefix:亚马逊存储空间要抓取资源的前缀。【可选】 37 | - -n/--max-keys:亚马逊每次列举请求返回数据的条目数量。【可选】 38 | - -m/--continuation-token:亚马逊接口返回的 token,用于断点列举。【可选】 39 | 40 | # 列举 41 | 使用场景: 42 | 列举亚马逊存储空间中所有的文件 43 | 44 | 假如要迁移的亚马逊账户的 Access Key ID, SecretKey 为: 45 | - AWS_ACCESS_KEY_ID = "12345" 46 | - AWS_SECRET_KEY = "6789" 47 | 48 | 亚马逊存储空间名为: 49 | AWS_BUCKET = "aws-bucket" 50 | 51 | 亚马逊空间所在地区为: 52 | AWS_REGION = "us-west-2" 53 | 54 | 可以使用如下命令进行列举: 55 | ``` 56 | $ qshell awslist -A 12345 -S 6789 aws-bucket us-west-2 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/b64decode.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed b64decode.md 6 | var b64DecodeDocument string 7 | 8 | const B64Decode = "b64decode" 9 | 10 | func init() { 11 | addCmdDocumentInfo(B64Decode, b64DecodeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/b64decode.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `b64decode` 命令用来将一段以 `Base64 编码` 或 `URL 安全的 Base64 编码` 的字符串解码。 3 | 4 | # 格式 5 | ``` 6 | qshell b64decode [-s|--safe] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell b64decode -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell b64decode --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - DataToDecode:待解码字符串。【必选】 24 | 25 | # 选项 26 | - -s/--safe:标志开启 urlsafe 的 base64 编码。【可选】 27 | 28 | # 示例 29 | 我们可以解码七牛上传凭证的第三部分,即编码后的 PutPolicy: 30 | ``` 31 | $ qshell b64decode 'eyJzY29wZSI6ImJiaW1nOnRlc3QucG5nIiwiZGVhZGxpbmUiOjE0MjcxODkxMzB9' 32 | {"scope":"bbimg:test.png","deadline":1427189130} 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/b64encode.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed b64encode.md 6 | var b64EncodeDocument string 7 | 8 | const B64Encode = "b64encode" 9 | 10 | func init() { 11 | addCmdDocumentInfo(B64Encode, b64EncodeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/b64encode.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `b64encode` 命令用来将一段字符串以 `Base64编码` 或 `URL安全的Base64编码` 格式进行编码。 3 | 4 | # 格式 5 | ``` 6 | qshell b64encode [-s|--s] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell b64encode -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell b64encode --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - DataToDecode:待编码字符串。【必选】 24 | 25 | # 选项 26 | - -s/--safe:标志开启 urlsafe 的 base64 编码。【可选】 27 | 28 | # 示例 29 | ``` 30 | $ qshell b64encode 'hello world' 31 | aGVsbG8gd29ybGQ= 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/batchchgm.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchchgm.md 6 | var batchChangeMimeTypeDocument string 7 | 8 | const BatchChangeMimeType = "batchchgm" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchChangeMimeType, batchChangeMimeTypeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchchlifecycle.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchchlifecycle.md 6 | var batchChangeLifecycleDocument string 7 | 8 | const BatchChangeLifecycle = "batchchlifecycle" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchChangeLifecycle, batchChangeLifecycleDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchchtype.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchchtype.md 6 | var batchChangeTypeDocument string 7 | 8 | const BatchChangeType = "batchchtype" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchChangeType, batchChangeTypeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchcopy.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchcopy.md 6 | var batchCopyDocument string 7 | 8 | const BatchCopyType = "batchcopy" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchCopyType, batchCopyDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchdelete.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchdelete.md 6 | var batchDeleteDocument string 7 | 8 | const BatchDeleteType = "batchdelete" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchDeleteType, batchDeleteDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchexpire.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchexpire.md 6 | var batchExpireDocument string 7 | 8 | const BatchExpireType = "batchexpire" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchExpireType, batchExpireDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchfetch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchfetch.md 6 | var batchFetchDocument string 7 | 8 | const BatchFetchType = "batchfetch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchFetchType, batchFetchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchforbidden.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchforbidden.md 6 | var batchForbiddenDocument string 7 | 8 | const BatchForbiddenType = "batchforbidden" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchForbiddenType, batchForbiddenDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchmatch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchmatch.md 6 | var batchMatchDocument string 7 | 8 | const BatchMatchType = "batchmatch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchMatchType, batchMatchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchmove.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchmove.md 6 | var batchMoveDocument string 7 | 8 | const BatchMoveType = "batchmove" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchMoveType, batchMoveDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchrename.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchrename.md 6 | var batchRenameDocument string 7 | 8 | const BatchRenameType = "batchrename" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchRenameType, batchRenameDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchrestorear.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchrestorear.md 6 | var batchRestoreArchiveDocument string 7 | 8 | const BatchRestoreArchiveType = "batchrestorear" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchRestoreArchiveType, batchRestoreArchiveDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchsign.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchsign.md 6 | var batchSignDocument string 7 | 8 | const BatchSignType = "batchsign" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchSignType, batchSignDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/batchsign.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `batchsign` 命令用来根据资源的公开外链生成对应的私有外链,用于七牛私有空间的文件访问外链批量生成。 3 | 4 | # 格式 5 | ``` 6 | qshell batchsign [-i ] [-e ] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell batchsign -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell batchsign --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - -i/--input-file:指定一个文件, 内容每行包含一个公开的外链。如果没有通过该选项指定该文件参数, 从标准输入读取内容。每行具体格式如下:(【可选】) 24 | ``` 25 | // 资源外链 26 | ``` 27 | - -o/--outfile:指定一个文件,把签名结果导入到此文件中【可选】 28 | - -e/--deadline:接受一个过时的 deadline 参数,如果没有指定该参数,默认为 3600s 。【必选】 29 | - --enable-record:记录任务执行状态,当下次执行命令时会检测任务执行的状态并跳过已执行的任务。 【可选】 30 | - --record-redo-while-error:依赖于 --enable-record;命令重新执行时,命令中所有任务会从头到尾重新执行;每个任务执行前会根据记录先查看当前任务是否已经执行,如果任务已执行且失败,则再执行一次;默认为 false,当任务执行失败则跳过不再重新执行。 【可选】 31 | 32 | # 示例 33 | 比如我们对文件`tosign.txt`里面的公开访问外链做签名。`tosign.txt`内容如下: 34 | ``` 35 | http://if-pri.qiniudn.com/camera.jpg 36 | http://if-pri.qiniudn.com/camera.jpg?imageView2/0/w/100 37 | ``` 38 | 使用 39 | ``` 40 | $ qshell batchsign -i tosign.txt 41 | ``` 42 | 就能生成私有外链: 43 | ``` 44 | http://if-pri.qiniudn.com/camera.jpg?e=1473840685&token=TQt-iplt8zbK3LEHMjNYyhh6PzxkbelZFRMl10MM:TnNXdt1Y4_jw-Xy0MF8vy9gF9dM= 45 | http://if-pri.qiniudn.com/camera.jpg?imageView2/0/w/100&e=1473840685&token=TQt-iplt8zbK3LEHMjNYyhh6PzxkbelZFRMl10MM:gjnUiiKUIOw7VQvJjYxXQLSybSM= 46 | ``` 47 | 或者指定外链的有效期时间戳: 48 | ``` 49 | $ qshell batchsign -i tosign.txt -e 1473840685 50 | ``` 51 | 这个时间戳可以用`d2ts`命令来生成。 52 | 53 | # 注意 54 | 如果没有指定输入文件,默认从标准输入读取内容 55 | -------------------------------------------------------------------------------- /docs/batchstat.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed batchstat.md 6 | var batchStatDocument string 7 | 8 | const BatchStatType = "batchstat" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BatchStatType, batchStatDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/bucket.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed bucket.md 6 | var bucketDocument string 7 | 8 | const BucketType = "bucket" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BucketType, bucketDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/bucket.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `bucket` 指令用来获取 bucket 信息。 3 | 4 | # 格式 5 | ``` 6 | qshell bucket 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell bucket -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell bucket --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:空间名称,可以为私有空间或者公开空间名称 【必选】 24 | 25 | # 选项 26 | 无 27 | 28 | # 示例 29 | 获取 my-bucket 空间的信息 30 | ``` 31 | $ qshell bucket my-bucket 32 | ``` 33 | 34 | 输出: 35 | ``` 36 | Bucket :my-bucket 37 | Region :z0 38 | Private :true 39 | ``` 40 | 41 | 输出字段说明: 42 | - Bucket:空间名。 43 | - Region:区域信息;z0:华东,z1:华北,z2:华南,na0:北美,as0:东南亚(具体参考:https://developer.qiniu.com/kodo/1671/region-endpoint-fq);默认为 z0。 44 | - Private:是否为私有空间 -------------------------------------------------------------------------------- /docs/buckets.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed buckets.md 6 | var bucketsDocument string 7 | 8 | const BucketsType = "buckets" 9 | 10 | func init() { 11 | addCmdDocumentInfo(BucketsType, bucketsDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/buckets.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `buckets` 指令可以获取当前账号下所有的空间名称并输出。 3 | 4 | # 格式 5 | ``` 6 | qshell buckets 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell buckets -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell buckets --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | 无参数。 24 | 25 | # 选项 26 | - --region:指定需要列举所在区域的 bucket。 27 | - --detail:打印 bucket 详情,如果无此选项则仅列举 bucket 名称,增加此选项后可依次展示 bucket 名、bucket 所在区域、bucket 文件数量、bucket 占用空间大小。 28 | 29 | # 示例 30 | 简单使用 31 | ``` 32 | $ qshell buckets 33 | ``` 34 | 输出: 35 | ``` 36 | bucket0 37 | bucket1 38 | bucket2 39 | bucket3 40 | bucket4 41 | bucket7 42 | ``` 43 | 44 | 列举所有区域 bucket 的详细信息 45 | ``` 46 | $ qshell buckets --detail 47 | ``` 48 | 输出: 49 | ``` 50 | bucket0 z0 0 0(0B) 51 | bucket1 z0 0 0(0B) 52 | bucket2 z0 0 0(0B) 53 | bucket3 z0 0 0(0B) 54 | bucket4 z0 0 0(0B) 55 | bucket7 z1 0 0(0B) 56 | ``` 57 | 58 | 列举 z0 区域中 bucket 的详细信息 59 | ``` 60 | $ qshell buckets --region z0 --detail 61 | ``` 62 | 输出: 63 | ``` 64 | bucket0 z0 0 0(0B) 65 | bucket1 z0 0 0(0B) 66 | bucket2 z0 0 0(0B) 67 | bucket3 z0 0 0(0B) 68 | bucket4 z0 0 0(0B) 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/cdnprefetch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed cdnprefetch.md 6 | var cdnPrefetchDocument string 7 | 8 | const CdnPrefetchType = "cdnprefetch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(CdnPrefetchType, cdnPrefetchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/cdnprefetch.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `cdnprefetch` 命令用来根据指定的文件访问列表来批量预取 CDN 的访问外链。 3 | 4 | # 格式 5 | ``` 6 | qshell cdnprefetch [-i ] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell cdnprefetch -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell cdnprefetch --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | 无 24 | 25 | # 选项 26 | - -i/--input-file:指定一个文件,文件内容每行包含一个文件访问外链。如果没有通过该选项指定该文件参数, 从标准输入读取内容。每行具体格式如下:【可选】 27 | ``` 28 | // :文件访问外链 29 | ``` 30 | - --qps:配置每秒预取的最大次数,默认不限制。【可选】 31 | - -s/--size:每批预取的最大 Url 数,最大 50;默认 50。【可选】 32 | 33 | # 示例 34 | 比如我们有如下内容的文件(`toprefetch.txt`),需要预取里面的外链 35 | ``` 36 | http://if-pbl.qiniudn.com/hello1.txt 37 | http://if-pbl.qiniudn.com/hello2.txt 38 | http://if-pbl.qiniudn.com/hello3.txt 39 | http://if-pbl.qiniudn.com/hello4.txt 40 | http://if-pbl.qiniudn.com/hello5.txt 41 | http://if-pbl.qiniudn.com/hello6.txt 42 | http://if-pbl.qiniudn.com/hello7.txt 43 | ``` 44 | 45 | 通过执行命令: 46 | ``` 47 | $ qshell cdnprefetch -i toprefetch.txt 48 | ``` 49 | 50 | 就可以预取文件 `toprefetch.txt` 中的访问外链了。 51 | -------------------------------------------------------------------------------- /docs/cdnrefresh.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed cdnrefresh.md 6 | var cdnRefreshDocument string 7 | 8 | const CdnRefreshType = "cdnrefresh" 9 | 10 | func init() { 11 | addCmdDocumentInfo(CdnRefreshType, cdnRefreshDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/cdnrefresh.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `cdnrefresh` 命令用来根据指定的文件访问列表或者目录列表来批量更新 CDN 的缓存。 3 | 4 | # 格式 5 | 刷新链接的命令格式: 6 | ``` 7 | qshell cdnrefresh [-i ] 8 | ``` 9 | 10 | 刷新目录的命令格式: 11 | ``` 12 | qshell cdnrefresh --dirs -i 13 | ``` 14 | 15 | 注意需要刷新的目录,必须以 `/` 结尾。如果没有制定输入文件 默认从终端读取输入内容 16 | 17 | # 帮助文档 18 | 可以在命令行输入如下命令获取帮助文档: 19 | ``` 20 | // 简单描述 21 | $ qshell cdnrefresh -h 22 | 23 | // 详细文档(此文档) 24 | $ qshell cdnrefresh --doc 25 | ``` 26 | 27 | # 鉴权 28 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 29 | 30 | # 参数 31 | 无 32 | 33 | # 选项 34 | - -i/--input-file:指定一个文件,文件内容每行包含一个需要进行刷新的外链。如果没有通过该选项指定文件参数, 则会从标准输入读取内容。每行具体格式如下:【可选】 35 | ``` 36 | // :访问外链,当指定了 -r/--dirs 选项时,Url 需为目录外联;未指定时,Url 需为文件外联 37 | ``` 38 | - -r, --dirs: 指定刷新外链类型为目录外链,无此选项为文件外链。【可选】 39 | - --qps:配置每秒预取的最大次数,默认不限制。【可选】 40 | - -s/--size:每批预取的最大 Url 数,最大 50;默认 50。【可选】 41 | 42 | 43 | # 示例 44 | ### 刷新多个文件外链: 45 | 比如我们有如下内容的文件(`torefresh.txt`),需要刷新里面的外链 46 | ``` 47 | http://if-pbl.qiniudn.com/hello1.txt 48 | http://if-pbl.qiniudn.com/hello2.txt 49 | http://if-pbl.qiniudn.com/hello3.txt 50 | http://if-pbl.qiniudn.com/hello4.txt 51 | http://if-pbl.qiniudn.com/hello5.txt 52 | http://if-pbl.qiniudn.com/hello6.txt 53 | http://if-pbl.qiniudn.com/hello7.txt 54 | ``` 55 | 56 | 通过执行命令: 57 | ``` 58 | $ qshell cdnrefresh -i torefresh.txt 59 | ``` 60 | 61 | 就可以刷新文件 `torefresh.txt` 中的访问外链了。 62 | -------------------------------------------------------------------------------- /docs/chgm.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed chgm.md 6 | var changeMimeDocument string 7 | 8 | const ChangeMimeType = "chgm" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ChangeMimeType, changeMimeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/chgm.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `chgm` 指令用来为空间中的一个文件修改 MimeType。 3 | 4 | 参考文档:[资源元信息修改 (chgm)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/chgm.html) 5 | 6 | # 格式 7 | ``` 8 | qshell chgm 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell chgm -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell chgm --doc 19 | ``` 20 | 21 | # 鉴权 22 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 23 | 24 | # 参数 25 | - Bucket:空间名,可以为公开空间或私有空间。【必须】 26 | - Key:空间中的文件名。【必须】 27 | - NewMimeType:给文件指定的新的 MimeType 。【必须】 28 | 29 | # 示例 30 | 修改 `if-pbl` 空间中 `qiniu.png` 图片的MimeType为 `image/jpeg` 31 | ``` 32 | $ qshell chgm if-pbl qiniu.png image/jpeg 33 | ``` 34 | 35 | 修改完成,我们检查一下文件的 MimeType: 36 | ``` 37 | $ qshell stat if-pbl qiniu.png 38 | ``` 39 | 40 | 输出 41 | ``` 42 | Bucket: if-pbl 43 | Key: qiniu.png 44 | Hash: FrUHIqhkDDd77-AtiDcOwi94YIeM 45 | Fsize: 5331 46 | PutTime: 14285516077733591 47 | MimeType: image/jpeg 48 | ``` 49 | 我们发现,文件的 MimeType 已经被修改为 `image/jpeg`。 50 | -------------------------------------------------------------------------------- /docs/chlifecycle.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed chlifecycle.md 6 | var changeLifecycleDocument string 7 | 8 | const ChangeLifecycle = "chlifecycle" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ChangeLifecycle, changeLifecycleDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/chtype.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed chtype.md 6 | var changeTypeDocument string 7 | 8 | const ChangeType = "chtype" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ChangeType, changeTypeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/chtype.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `chtype` 指令用来为空间中的一个文件修改 **存储类型**。 3 | 4 | # 格式 5 | ``` 6 | qshell chtype 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell chtype -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell chtype --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:空间名,可以为公开空间或私有空间。【必选】 24 | - Key:空间中的文件名。【必选】 25 | - FileType:给文件指定的新的存储类型,其中可选值为 `0` 代表 `普通存储`,`1` 代表 `低频存储`,`2` 代表 `归档存储`,`3` 代表 `深度归档存储`,`4` 代表 `归档直读存储`。【必选】 26 | 27 | 注: 28 | `归档存储` 或 `深度归档存储` 直接转 `普通存储` 或 `低频存储` 会失败,需要先通过 restorear 命令恢复后再转。 29 | 30 | # 示例 31 | 修改 `if-pbl` 空间中 `qiniu.png` 图片的存储类型为 `低频存储` 32 | ``` 33 | $ qshell chtype if-pbl qiniu.png 1 34 | ``` 35 | 36 | 修改完成,我们检查一下文件的存储类型: 37 | ``` 38 | $ qshell stat if-pbl qiniu.png 39 | ``` 40 | 41 | 输出 42 | ``` 43 | Bucket: if-pbl 44 | Key: qiniu.png 45 | Hash: FrUHIqhkDDd77-AtiDcOwi94YIeM 46 | Fsize: 5331 47 | PutTime: 14285516077733591 48 | MimeType: image/jpeg 49 | FileType: 1 -> 低频存储 50 | ``` 51 | 我们发现,文件的存储类型已经被修改为 `低频存储` 了。 52 | -------------------------------------------------------------------------------- /docs/copy.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed copy.md 6 | var copyDocument string 7 | 8 | const CopyType = "copy" 9 | 10 | func init() { 11 | addCmdDocumentInfo(CopyType, copyDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/copy.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `copy` 命令用来为存储在七牛空间中的文件创建副本。注意如果目标文件已存在空间中的时候,默认情况下,`copy` 会失败,报错 `614 file exists`,如果一定要强制覆盖目标文件,可以使用选项 `--overwrite` 。 3 | 4 | 注: 5 | 原空间和目标空间必须属于同一个区域。 6 | 7 | 参考文档:[资源复制 (copy)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/copy.html) 8 | 9 | # 格式 10 | ``` 11 | qshell copy [--overwrite] [-k ] 12 | ``` 13 | 14 | # 帮助文档 15 | 可以在命令行输入如下命令获取帮助文档: 16 | ``` 17 | // 简单描述 18 | $ qshell copy -h 19 | 20 | // 详细文档(此文档) 21 | $ qshell copy --doc 22 | ``` 23 | 24 | # 鉴权 25 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 26 | 27 | # 参数 28 | - SrcBucket: 源空间名称 【必选】 29 | - SrcKey: 源文件名称 【必选】 30 | - DestBucket: 目标空间名称,可以和源空间名称相同【必选】 31 | 32 | # 选项 33 | - -k/--key: 目标文件名称(DestKey),如果是 `DestBucket` 和 `SrcBucket` 不同的情况下,这个参数可以不填,默认和 `SrcKey` 相同。【可选】 34 | - --overwrite: 当保存的文件已存在时,强制用新文件覆盖原文件,如果无此选项操作会失败。【可选】 35 | 36 | ##### 备注: 37 | 1 如果复制的副本和原文件在同一个空间,那么必须提供不同于原文件的副本文件名,或者加上覆盖选项 `--overwrite` 38 | 2 如果复制的副本和原文件不在同一个空间,那么可以不提供副本文件名,默认和原文件名相同。 39 | 3 不支持跨存储区域复制文件, SrcBucket, DestBucket必须在统一存储区域 40 | 41 | # 描述 42 | 1 复制 `if-pbl` 空间中的 `qiniu.jpg`,并保存在 `if-pbl` 中,新副本文件名为 `2015/01/19/qiniu.jpg` 43 | ``` 44 | $ qshell copy if-pbl qiniu.jpg if-pbl -k 2015/01/19/qiniu.jpg 45 | ``` 46 | 47 | 2 复制 `if-pbl` 空间中的 `qiniu.jpg`,并保存在 `if-pri` 中,新副本文件名和原文件名相同 48 | ``` 49 | $ qshell copy if-pbl qiniu.jpg if-pri 50 | ``` 51 | 52 | 3 复制 `if-pbl` 空间中的 `qiniu.jpg`,并保存到 `if-pri` 空间中,保存 Key 为:`qiniu_pri.jpg`,由于 `if-pri` 已有文件 `qiniu_pri.jpg`,所以加上选项 `--overwrite` 强制覆盖 53 | ``` 54 | $ qshell copy --overwrite if-pbl qiniu.jpg if-pri -k qiniu_pri.jpg 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/create-share.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed create-share.md 6 | var createShareDocument string 7 | 8 | const CreateShareType = "create-share" 9 | 10 | func init() { 11 | addCmdDocumentInfo(CreateShareType, createShareDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/create-share.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `create-share` 命令为需要分享的目录或前缀创建授权链接。 3 | 4 | # 格式 5 | ``` 6 | qshell create-share [kodo://]/ 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell create-share -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell create-share --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket: 空间名称【必选】 24 | - Prefix: 前缀,注意如果要分享云存储内的目录,则必须以 `/` 结尾【必选】 25 | 26 | # 选项 27 | - --extract-code: 提取码,只能包含六位大小写字母或者数字,如果不填写,将会自动生成。【可选】 28 | - --validity-period: 有效时间,如果不填写,默认为 15 分钟。【可选】 29 | - --output: 保存路径,以 JSON 格式保存输出内容,如果不填写,则直接以文本形式输出。【可选】 30 | 31 | # 示例 32 | ``` 33 | $ qshell create-share kodo://bucketname/prefix 34 | Link: 35 | http://portal.qiniu.com/kodo-shares/verify?id=AGQEKDRxBBjbGmsKduQS9oFx59rz&token=qhtbC5YmDCO-WiPriuoCG_t4hZ1LboSOtRYSJXo_%3A9uJY8FiNrKjNrt4MpBx547jlgwr8aes15z5i8VY6l5SU6ga2IKWDBSGTv1jo-rOocklE7QqApzG6okJktZ36umLoqv9x1kuo5fNmgasLXowyTuHIM3kXsaV_DoXmvQsGr5ol6j4RtrmLcKdtXhpkGH8MfSjEgRV91Bx_Q_mSwpJ1028p8yZCSad_QOu_kSPxzeLZmWlUpAtO2oEXdbMTBxhTCH_3awCgqkgoogi0FQGP4zHxeFr0n3vj69DpmWqe6DiYbYLivCuU0kOF5Khv4I6-w6vjjdY 36 | Extract Code: 37 | wp7gqc 38 | Expire: 39 | 2024-10-09 10:44:41 +0000 40 | 41 | $ qshell create-share --output=share.json kodo://bucketname/prefix 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/d2ts.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed d2ts.md 6 | var dateToTimestampDocument string 7 | 8 | const DateToTimestampType = "d2ts" 9 | 10 | func init() { 11 | addCmdDocumentInfo(DateToTimestampType, dateToTimestampDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/d2ts.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `d2ts` 命令用来生成一个 SecondsToNow 秒后的 Unix 时间戳(单位秒)。 3 | 4 | # 格式 5 | ``` 6 | qshell d2ts 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell d2ts -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell d2ts --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - SecondsToNow: 指定的秒数 24 | 25 | # 示例 26 | ``` 27 | $ qshell d2ts 3600 28 | 1427252311 29 | ``` -------------------------------------------------------------------------------- /docs/delete.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed delete.md 6 | var deleteDocument string 7 | 8 | const DeleteType = "delete" 9 | 10 | func init() { 11 | addCmdDocumentInfo(DeleteType, deleteDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/delete.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `delete` 命令用来从七牛的空间里面删除一个文件。 3 | 4 | 参考文档:[资源删除 (delete)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/delete.html) 5 | 6 | # 格式 7 | ``` 8 | qshell delete 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell delete -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell delete --doc 19 | ``` 20 | 21 | # 鉴权 22 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 23 | 24 | # 参数 25 | - Bucket:空间名,可以为公开空间或私有空间【必选】 26 | - Key:空间中的文件名【必选】 27 | 28 | # 示例 29 | 删除空间 `if-pbl` 里面的视频 `qiniu.mp4` 30 | ``` 31 | qshell delete if-pbl qiniu.mp4 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/dircache.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed dircache.md 6 | var dirCacheDocument string 7 | 8 | const DirCacheType = "dircache" 9 | 10 | func init() { 11 | addCmdDocumentInfo(DirCacheType, dirCacheDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/dircache.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `dircache` 用来为指定的本地目录生成一个该目录下面所有的文件的列表,文件列表的每行分别为每个文件的相对路径,文件大小和最后修改时间,该文件列表可以作为 `qupload` 命令的 `file_list` 的参数。你可以修改该命令生成的文件,删除一些行,然后给 `qupload` 的参数 `file_list`,这样就可以只上传指定的文件列表。 3 | 4 | # 格式 5 | ``` 6 | qshell dircache [-o ] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell dircache -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell dircache --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - DirCacheRootPath:本地需要对其做快照的路径,最好是绝对路径,比如 `/Users/jemy/Demo1` 这样的路径【必选】 24 | - DirCacheResultFile:快照结果的保存文件,可以是绝对路径或者相对路径, 如果没有指定,默认输出到标准输出终端。 【可选】 25 | 26 | # 示例 27 | 比如,要获取 `/Users/jemy/Temp4` 目录下面的文件列表,则使用 28 | ``` 29 | qshell dircache /Users/jemy/Temp4 -o temp4.list.txt 30 | ``` 31 | 32 | 其中 `temp4.list.txt` 是你保存列表结果的文件。列举的结果以如下格式组织: 33 | ``` 34 | 文件相对于的相对路径\t文件大小(单位字节)\t文件上次修改时间(单位100纳秒) 35 | ``` 36 | 37 | 比如这样的: 38 | ``` 39 | rk_video_not_play.mp4 3985210 14206026340000000 40 | rtl1.flv 10342916 14205959890000000 41 | sync_demo/array_enumeration.png 5262899 13953255140000000 42 | sync_demo/demo2.gif 2685960 13966636230000000 43 | sync_demo/golang.png 149366 14010291080000000 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/domains.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed domains.md 6 | var domainsDocument string 7 | 8 | const DomainsType = "domains" 9 | 10 | func init() { 11 | addCmdDocumentInfo(DomainsType, domainsDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/domains.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `domains` 指令可以根据指定的空间参数获取和该空间关联的所有域名并输出。 3 | 4 | # 格式 5 | ``` 6 | qshell domains [--detail] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell domains -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell domains --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 24 | 25 | # 选项 26 | --detail:展示域名的详细信息,默认只展示域名的名称。【可选】 27 | 28 | # 示例 29 | 获取空间 `if-pbl` 对应的所有域名: 30 | ``` 31 | $ qshell domains if-pbl 32 | ``` 33 | 34 | 输出: 35 | ``` 36 | if-pbl.qiniudn.com 37 | 7pn64c.com1.z0.glb.clouddn.com 38 | 7pn64c.com2.z0.glb.clouddn.com 39 | 7pn64c.com2.z0.glb.qiniucdn.com 40 | ``` -------------------------------------------------------------------------------- /docs/expire.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed expire.md 6 | var expireDocument string 7 | 8 | const ExpireType = "expire" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ExpireType, expireDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/expire.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `expire` 指令用来为空间中的一个文件修改 **过期时间**。(即多长时间后,该文件会被自动删除) 3 | 4 | ## 注: 5 | v2.7.0 到 v2.9.2 不支持取消过期时间配置,如果过期天数配置为 0 会立即删除文件。 6 | v2.10.0 开始,过期天数配置为 0 时为取消过期时间配置。 7 | 8 | # 格式 9 | ``` 10 | qshell expire 11 | ``` 12 | 13 | # 帮助文档 14 | 可以在命令行输入如下命令获取帮助文档: 15 | ``` 16 | // 简单描述 17 | $ qshell expire -h 18 | 19 | // 详细文档(此文档) 20 | $ qshell expire --doc 21 | ``` 22 | 23 | # 鉴权 24 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 25 | 26 | # 参数 27 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 28 | - Key:空间中的文件名【必选】 29 | - DeleteAfterDays:给文件指定的新过期时间,范围:大于等于 0,0:取消过期时间设置。单位为:天【必选】 30 | 31 | # 示例 32 | 修改 `if-pbl` 空间中 `qiniu.png` 图片的过期时间为:`3天后自动删除` 33 | ``` 34 | $ qshell expire if-pbl qiniu.png 3 35 | ``` 36 | 输入该命令,后该文件就已经被修改为 3 天后自动删除了。 37 | -------------------------------------------------------------------------------- /docs/fetch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed fetch.md 6 | var fetchDocument string 7 | 8 | const FetchType = "fetch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(FetchType, fetchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/fetch.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `fetch` 指令根据七牛的公开API [fetch](http://developer.qiniu.com/code/v6/api/kodo-api/rs/fetch.html) 来从互联网上抓取一个资源并保存到七牛的空间中。 3 | 每次抓取的资源,如果指定的 Key 都是一样的,那么会默认覆盖这个 Key 所对应的文件。 4 | 5 | 参考文档:[第三方资源抓取 (fetch)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/fetch.html) 6 | 7 | # 格式 8 | ``` 9 | qshell fetch [-k ] 10 | ``` 11 | 12 | # 帮助文档 13 | 可以在命令行输入如下命令获取帮助文档: 14 | ``` 15 | // 简单描述 16 | $ qshell fetch -h 17 | 18 | // 详细文档(此文档) 19 | $ qshell fetch --doc 20 | ``` 21 | 22 | # 鉴权 23 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 24 | 25 | # 参数 26 | - RemoteResourceUrl:互联网上资源的链接,必须是可访问的链接【必选】 27 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 28 | - Key:该资源保存在空间中的名字,如果不指定这个名字,那么会使用抓取的资源的内容 `hash` 值来作为文件名【可选】 29 | 30 | # 示例 31 | 1 抓取一个资源并以指定的文件名保存在七牛的空间里面 32 | ``` 33 | $ qshell fetch https://www.baidu.com/img/bdlogo.png if-pbl -k bdlogo.png 34 | 35 | Key: bdlogo.png 36 | Hash: FrUHIqhkDDd77-AtiDcOwi94YIeM 37 | Fsize: 5331 (5.21 KB) 38 | Mime: image/png 39 | ``` 40 | 41 | 2 抓取一个资源并使用文件的内容 `hash` 值来作为文件名保存在七牛的空间中 42 | ``` 43 | $ qshell fetch https://www.baidu.com/img/bdlogo.png if-pbl 44 | 45 | Key: FrUHIqhkDDd77-AtiDcOwi94YIeM 46 | Hash: FrUHIqhkDDd77-AtiDcOwi94YIeM 47 | Fsize: 5331 (5.21 KB) 48 | Mime: image/png 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/forbidden.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed forbidden.md 6 | var forbiddenDocument string 7 | 8 | const ForbiddenType = "forbidden" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ForbiddenType, forbiddenDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/forbidden.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `forbidden` 禁用指定文件,禁用之后该文件不可访问。 3 | 4 | # 格式 5 | ``` 6 | qshell forbidden [flags] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell forbidden -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell forbidden --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | * Bucket:空间名,可以为公开空间或私有空间。【必须】 24 | * Key:空间中的文件名。【必须】 25 | 26 | # 选项 27 | * -r/--reverse : 启用指定文件 【可选】 28 | 29 | # 示例 30 | ``` 31 | $ qshell forbidden test-bucket test-file 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/fput.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed fput.md 6 | var formPutDocument string 7 | 8 | const FormPutType = "fput" 9 | 10 | func init() { 11 | addCmdDocumentInfo(FormPutType, formPutDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/func.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed func.md 6 | var funcDocument string 7 | 8 | const FuncType = "func" 9 | 10 | func init() { 11 | addCmdDocumentInfo(FuncType, funcDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/func.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `func` 命令是封装 Go 语言的模板功能,你可以使用此命令进行模板相关的处理。 3 | 4 | 具体参考: 5 | https://pkg.go.dev/text/template 6 | https://masterminds.github.io/sprig 7 | 8 | qshell 有些命令的回调使用模板方式实现,在使用之前可以使用此命令验证模板格式是否预期。 9 | 10 | 比如:在批量下载文件时,用户根据需要调整下载路径的回调; 是由工具内部传递包含有下载信息,用户仅需要编写 作为下载命令的参数,模板相当于回调函数,在函数中通过输出告诉 qshell 用户所期望的值(输出即 return 值)。 11 | 12 | 注: 13 | 输出的 [] 仅仅为了方便用户了解输出文字的边界,真正结果不包含 []。 14 | 15 | # 格式 16 | ``` 17 | qshell func [flags] 18 | ``` 19 | 20 | # 帮助文档 21 | 可以在命令行输入如下命令获取帮助文档: 22 | ``` 23 | // 简单描述 24 | $ qshell func -h 25 | 26 | // 详细文档(此文档) 27 | $ qshell func --doc 28 | ``` 29 | 30 | # 鉴权 31 | 无 32 | 33 | # 参数 34 | - ParamsJson:Go 语言模板功能输入的参数 【必选】 35 | - FuncTemplate:Go 语言的模板 【必选】 36 | 37 | # 选项 38 | 无 39 | 40 | # 示例 41 | ``` 42 | 1. 截取 "this is a test" 中尾部的 test 43 | $qshell func '{"name":"this is a test"}' '{{trimSuffix "test" .name}}' 44 | [W] output is insert [], and you should be careful with spaces etc. 45 | [I] [this is a ] 46 | 47 | 解析: 48 | '{"name":"this is a test"}' 为参数,json 结构为字典 49 | '{{trimSuffix "test" .name}}' 为模板,{{}}表示内部有函数调用,trimSuffix 为函数名, "test" 为需要去除的尾部字符串,. 表示当前参数(即上面字典),name 表示字典中的 key,通过 .name 获取当前字典中键 name 对应的 key。 50 | 注意:' 和 { / } 之间没有其他符号,如果有则输出也会有(比如:'lala {{trimSuffix "test" .name}}', 输出为:[lala this is a ]),其他符号包含空格等。 51 | 52 | 53 | 2. 截取 "this is a test" 中首部的 this 54 | $qshell func '{"name":"this is a test"}' '{{trimPrefix "this" .name}}' 55 | [W] output is insert [], and you should be careful with spaces etc. 56 | [I] [ is a test] 57 | 58 | 59 | 3. 获取 "this is a test" 中从下标 0 到下标 5 的部分,包含下标 5。 60 | qshell func '{"name":"this is a test"}' '{{substr 0 6 .name}}' 61 | [W] output is insert [], and you should be careful with spaces etc. 62 | [I] [this i] 63 | 64 | ``` -------------------------------------------------------------------------------- /docs/get.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed get.md 6 | var getDocument string 7 | 8 | const GetType = "get" 9 | 10 | func init() { 11 | addCmdDocumentInfo(GetType, getDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/ip.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed ip.md 6 | var ipDocument string 7 | 8 | const IPType = "ip" 9 | 10 | func init() { 11 | addCmdDocumentInfo(IPType, ipDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/ip.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `ip` 是一个查询 ip 归属地和运营商的工具,根据淘宝的 API 来获取结果。 3 | 4 | API 地址:[http://ip.taobao.com/service/getIpInfo.php](http://ip.taobao.com/service/getIpInfo.php) 。该工具一次可以查询多个 ip 的信息。 5 | 6 | # 格式 7 | ``` 8 | qshell ip [ [ ...]]] 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell ip -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell ip --doc 19 | ``` 20 | 21 | # 鉴权 22 | 无 23 | 24 | # 参数 25 | - Ip1:第一个ip地址 【必选】 26 | - Ip2:第二个ip地址 【可选】 27 | - Ip3:第三个ip地址 【可选】 28 | - IpN:第N个ip地址 【可选】 29 | 30 | # 示例 31 | 查询 `180.154.236.170` 和 `192.168.1.1` 的归属地和运营商。 32 | ``` 33 | qshell ip 180.154.236.170 192.168.1.1 34 | ``` 35 | 36 | 输出: 37 | ``` 38 | IP :180.154.236.170 39 | Country :中国 40 | Area : 41 | Region :上海 42 | City :上海 43 | Isp :电信 44 | 45 | IP :192.168.1.1 46 | Country :局域网 47 | Area : 48 | Region :局域网 49 | City : 50 | Isp :局域网 51 | ``` -------------------------------------------------------------------------------- /docs/listbucket.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed listbucket.md 6 | var listBucketDocument string 7 | 8 | const ListBucketType = "listbucket" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ListBucketType, listBucketDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/listbucket2.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed listbucket2.md 6 | var listBucket2Document string 7 | 8 | const ListBucket2Type = "listbucket2" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ListBucket2Type, listBucket2Document) 12 | } 13 | -------------------------------------------------------------------------------- /docs/m3u8delete.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed m3u8delete.md 6 | var m3u8DeleteDocument string 7 | 8 | const M3u8DeleteType = "m3u8delete" 9 | 10 | func init() { 11 | addCmdDocumentInfo(M3u8DeleteType, m3u8DeleteDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/m3u8delete.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `m3u8delete` 命令用来根据七牛空间中 m3u8 播放列表文件名字来删除空间中的 m3u8 播放列表文件和所引用的所有切片文件。 3 | 4 | # 格式 5 | ``` 6 | qshell m3u8delete 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell m3u8delete -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell m3u8delete --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:m3u8 文件所在空间,可以为公开空间或私有空间 【必选】 24 | - M3u8Key:m3u8 文件的名字 【必选】 25 | 26 | # 示例 27 | 1 删除公开空间中 m3u8 文件及其所引用的所有切片文件。 28 | ``` 29 | qshell m3u8delete if-pbl qiniu.m3u8 30 | ``` 31 | 32 | 2 删除私有空间中 m3u8 文件及其所引用的所有切片文件。 33 | ``` 34 | qshell m3u8delete if-pri qiniu.m3u8 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/m3u8replace.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed m3u8replace.md 6 | var m3u8ReplaceDocument string 7 | 8 | const M3u8ReplaceType = "m3u8replace" 9 | 10 | func init() { 11 | addCmdDocumentInfo(M3u8ReplaceType, m3u8ReplaceDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/m3u8replace.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `m3u8replace` 命令用来修改或删除七牛空间中 m3u8 播放列表中引用的切片路径中的域名。 3 | 4 | # 格式 5 | ``` 6 | qshell m3u8replace [] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell m3u8replace -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell m3u8replace --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:m3u8 文件所在空间,可以为公开空间或私有空间 【必选】 24 | - M3u8Key:m3u8 文件的名字 【必选】 25 | - NewDomain:引用切片的域名,如果不指定的话,则 m3u8 文件中引用切片使用相对路径,效果等同于转码时指定 `noDomain/1` 【可选】 26 | 27 | # 示例 28 | 1 清除 m3u8 播放列表中切片引用路径中的域名,等同于转码时指定 `noDomain/1` 29 | ``` 30 | qshell m3u8replace if-pbl qiniu.m3u8 31 | ``` 32 | 33 | 2 替换 m3u8 播放列表中切片引用路径中的域名,把旧的换成新的。 34 | ``` 35 | qshell m3u8replace if-pbl qiniu.m3u8 http://hls.example.com 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/match.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed match.md 6 | var matchDocument string 7 | 8 | const MatchType = "match" 9 | 10 | func init() { 11 | addCmdDocumentInfo(MatchType, matchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/match.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `match` 指令用来验证本地文件和七牛云存储文件是否匹配。 3 | 4 | # 格式 5 | ``` 6 | qshell match 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell match -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell match --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 24 | - Key:空间中文件的名称【必选】 25 | - LocalFile:本地文件路径 【必选】 26 | 27 | # 示例 28 | ``` 29 | $ qshell match if-pbl qiniu.png /Users/lala/Desktop/qiniu.png 30 | ``` -------------------------------------------------------------------------------- /docs/mirrorupdate.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed mirrorupdate.md 6 | var mirrorUpdateDocument string 7 | 8 | const MirrorUpdateType = "mirrorupdate" 9 | 10 | func init() { 11 | addCmdDocumentInfo(MirrorUpdateType, mirrorUpdateDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/mirrorupdate.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `mirrorupdate` 指令用来更新七牛空间中的某个文件。配置了镜像存储的空间,在一个文件首次回源源站拉取资源后,就不再回源了。如果源站更新了一个文件,那么这个文件不会自动被同步更新到七牛空间,这个时候需要使用 `mirrorupdate` 去主动拉取一次这个文件的新内容回来覆盖七牛空间中的旧文件。 3 | 4 | 功能同 `prefetch` 5 | 6 | 参考文档:[镜像资源更新 (prefetch)](http://developer.qiniu.com/docs/v6/api/reference/rs/prefetch.html) 7 | 8 | # 格式 9 | ``` 10 | qshell mirrorupdate 11 | ``` 12 | 13 | # 帮助文档 14 | 可以在命令行输入如下命令获取帮助文档: 15 | ``` 16 | // 简单描述 17 | $ qshell mirrorupdate -h 18 | 19 | // 详细文档(此文档) 20 | $ qshell mirrorupdate --doc 21 | ``` 22 | 23 | # 鉴权 24 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 25 | 26 | # 参数 27 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 28 | - Key:空间中文件的名称【必选】 29 | 30 | # 示例 31 | ``` 32 | $ qshell mirrorupdate if-pbl qiniu.png 33 | ``` -------------------------------------------------------------------------------- /docs/mkbucket.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed mkbucket.md 6 | var mkBucketDocument string 7 | 8 | const MkBucketType = "mkBucketDocument" 9 | 10 | func init() { 11 | addCmdDocumentInfo(MkBucketType, mkBucketDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/mkbucket.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `mkbucket` 指令用来创建一个 bucket。 3 | 4 | # 格式 5 | ``` 6 | qshell mkbucket [--region Region] [--private] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell mkbucket -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell mkbucket --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Bucket:空间名,可以为私有空间或者公开空间名称 【必选】 24 | 25 | # 选项 26 | - --region:指定创建 bucket 所在的区域;z0:华东,z1:华北,z2:华南,na0:北美,as0:东南亚(具体参考:https://developer.qiniu.com/kodo/1671/region-endpoint-fq);默认为 z0。【可选】 27 | - --private:是否创建私有空间;默认创建公有空间。【可选】 28 | 29 | # 示例 30 | 在华北区域创建名为 my-bucket 的私有空间 31 | ``` 32 | $ qshell mkbucket my-bucket --region z1 --private 33 | ``` -------------------------------------------------------------------------------- /docs/move.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed move.md 6 | var moveDocument string 7 | 8 | const MoveType = "move" 9 | 10 | func init() { 11 | addCmdDocumentInfo(MoveType, moveDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/move.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `move` 命令可以将一个空间中的文件移动到另外一个空间中,也可以对同一空间中的文件重命名。移动文件仅支持在同一个帐号下面的同区域空间中移动。 3 | 注意如果目标文件已存在空间中的时候,默认情况下,`move` 会失败,报错 `614 file exists`,如果一定要强制覆盖目标文件,可以使用选项 `--overwrite` 。 4 | 5 | 参考文档:[资源移动/重命名 (move)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/move.html) 6 | 7 | # 格式 8 | ``` 9 | qshell move [--overwrite] [-k DestKey] 10 | ``` 11 | 12 | # 帮助文档 13 | 可以在命令行输入如下命令获取帮助文档: 14 | ``` 15 | // 简单描述 16 | $ qshell move -h 17 | 18 | // 详细文档(此文档) 19 | $ qshell move --doc 20 | ``` 21 | 22 | # 鉴权 23 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 24 | 25 | # 参数 26 | - SrcBucket: 源空间名称 27 | - SrcKey: 源文件名称 28 | - DestBucket: 目标空间名称 29 | 30 | # 选项 31 | - -k/--key: 目标文件名称(DestKey),如果是 `DestBucket` 和 `SrcBucket` 不同的情况下,这个参数可以不填,默认和 `SrcKey` 相同。【可选】 32 | - --overwrite: 当保存的文件已存在时,强制用新文件覆盖原文件,如果无此选项操作会失败。【可选】 33 | 34 | # 示例 35 | 1 将空间 `if-pbl` 中的 `qiniu.jpg` 移动到 `if-pri` 中 36 | ``` 37 | qshell move if-pbl qiniu.jpg if-pri qiniu.jpg 38 | ``` 39 | 40 | 2 将空间 `if-pbl` 中的 `qiniu.jpg` 重命名为 `2015/01/19/qiniu.jpg` 41 | ``` 42 | qshell move if-pbl qiniu.jpg if-pbl 2015/01/19/qiniu.jpg 43 | ``` 44 | 45 | 3 将空间 `if-pbl` 中的 `qiniu.jpg` 移动到 `if-pri` 中,并命名为 `2015/01/19/qiniu.jpg` 46 | ``` 47 | qshell move if-pbl qiniu.jpg if-pri 2015/01/19/qiniu.jpg 48 | ``` 49 | 50 | 4 强制覆盖 `if-pbl` 中的已有文件 `2015/01/19/qiniu.jpg` 51 | ``` 52 | qshell move --overwrite if-pbl qiniu.jpg if-pbl 2015/01/19/qiniu.jpg 53 | ``` 54 | 执行命令之后,此时空间 `if-pbl` 里面的 `qiniu.jpg` 文件内容覆盖空间 `if-pbl` 里面的 `2015/01/19/qiniu.jpg`,`2015/01/19/qiniu.jpg` 文件原有内容完全被`qiniu.jpg` 文件覆盖,即空间 `if-pbl` 里面的 `qiniu.jpg` 文件此后已不存在,最后剩下 `2015/01/19/qiniu.jpg` 文件,文件内容是 `qiniu.jpg` 文件的内容。可以简单理解为鸠占鹊巢。 55 | -------------------------------------------------------------------------------- /docs/pfop.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed pfop.md 6 | var pFopDocument string 7 | 8 | const PFopType = "pfop" 9 | 10 | func init() { 11 | addCmdDocumentInfo(PFopType, pFopDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/pfop.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `pfop` 用来提交异步处理音视频请求, 比如视频转码,水印等, 打印服务端返回的 `PersistentID` 到标准输出上(终端), 可以根据该 `PersistentID` 查询处理进度 3 | 4 | 参考文档:[pfop请求](http://developer.qiniu.com/code/v6/api/dora-api/pfop/pfop.html) 5 | 6 | # 格式 7 | ``` 8 | qshell pfop [--pipeline ] 9 | qshell pfop [--pipeline ] --workflow-template-id 10 | ``` 11 | 12 | # 帮助文档 13 | 可以在命令行输入如下命令获取帮助文档: 14 | ``` 15 | // 简单描述 16 | $ qshell pfop -h 17 | 18 | // 详细文档(此文档) 19 | $ qshell pfop --doc 20 | ``` 21 | 22 | # 参数 23 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 24 | - Key:空间中文件的名称【必选】 25 | - Fops:数据处理命令列表,以;分隔,可以指定多个数据处理命令。如果没有指定 `--workflow-template-id` 选项,则必选。 26 | 如: `avthumb/mp4|saveas/cWJ1Y2tldDpxa2V5;avthumb/flv|saveas/cWJ1Y2tldDpxa2V5Mg==`,是将上传的视频文件同时转码成mp4格式和flv格式后另存。 27 | 28 | # 选项 29 | - -p/--pipeline:处理队列名称, 如果没有制定该选项,默认使用公有队列【可选】 30 | - -u/--notify-url:处理结果通知接收 URL,七牛将会向你设置的 URL 发起 Content-Type: application/json 的 POST 请求。【可选】 31 | - -y/--force:强制执行数据处理。当服务端发现 fops 指定的数据处理结果已经存在,那就认为已经处理成功,避免重复处理浪费资源。 增加此选项(--force),则可强制执行数据处理并覆盖原结果。【可选】 32 | - --workflow-template-id:工作流模版 ID【可选】 33 | - --type:任务类型【可选】 34 | 35 | # 示例 36 | 1 把 qiniutest 空间下的文件 `test.avi` 转码成 `mp4` 文件,转码后的结果保存到 `qiniutest` 空间中 37 | ``` 38 | $ qshell pfop qiniutest test.avi 'avthumb/mp4' 39 | ``` 40 | 41 | 返回 `PersistentID` 比如: 42 | ``` 43 | z1.5be96c32856db80b4be3d8b6 44 | ``` 45 | 46 | 可以使用如下的命令查看处理进度 47 | ``` 48 | $ qshell prefop z1.5be96c32856db80b4be3d8b6 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/prefetch.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed prefetch.md 6 | var prefetchDocument string 7 | 8 | const PrefetchType = "prefetch" 9 | 10 | func init() { 11 | addCmdDocumentInfo(PrefetchType, prefetchDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/prefetch.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `prefetch` 指令用来更新七牛空间中的某个文件。配置了镜像存储的空间,在一个文件首次回源源站拉取资源后,就不再回源了。如果源站更新了一个文件,那么这个文件不会自动被同步更新到七牛空间,这个时候需要使用 `mirrorupdate` 去主动拉取一次这个文件的新内容回来覆盖七牛空间中的旧文件。 3 | 4 | 功能同 `mirrorupdate` 5 | 6 | 参考文档:[镜像资源更新 (prefetch)](http://developer.qiniu.com/docs/v6/api/reference/rs/prefetch.html) 7 | 8 | # 格式 9 | ``` 10 | qshell prefetch 11 | ``` 12 | 13 | # 帮助文档 14 | 可以在命令行输入如下命令获取帮助文档: 15 | ``` 16 | // 简单描述 17 | $ qshell prefetch -h 18 | 19 | // 详细文档(此文档) 20 | $ qshell prefetch --doc 21 | ``` 22 | 23 | # 鉴权 24 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 25 | 26 | # 参数 27 | - Bucket:空间名,可以为公开空间或者私有空间【必选】 28 | - Key:空间中文件的名称【必选】 29 | 30 | # 示例 31 | ``` 32 | $ qshell prefetch if-pbl qiniu.png 33 | ``` -------------------------------------------------------------------------------- /docs/prefop.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed prefop.md 6 | var preFopDocument string 7 | 8 | const PreFopType = "prefop" 9 | 10 | func init() { 11 | addCmdDocumentInfo(PreFopType, preFopDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/prefop.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `prefop` 命令用来使用 [pfop请求](http://developer.qiniu.com/code/v6/api/dora-api/pfop/pfop.html) 得到的 `PersistentId` 来查询七牛数据处理的状态或结果。 3 | 4 | 参考文档:[持久化处理状态查询 (prefop)](http://developer.qiniu.com/code/v6/api/dora-api/pfop/prefop.html) 5 | 6 | # 格式 7 | ``` 8 | qshell prefop 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell prefop -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell prefop --doc 19 | ``` 20 | 21 | # 鉴权 22 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 23 | 24 | # 参数 25 | - PersistentId:持久化处理的 Id【必选】 26 | 27 | # 选项 28 | - --bucket: 持久化处理的文件所在的 bucket;公有云可选,私有云当没有配置 api host 时必选。 29 | 30 | # 示例 31 | 查询 `z0.58632a1945a2650cfd5fc8b1` 对应的持久化处理结果: 32 | ``` 33 | qshell prefop z0.58632a1945a2650cfd5fc8b1 34 | ``` 35 | 36 | 输出 37 | ``` 38 | Id: z0.58632a1945a2650cfd5fc8b1 39 | Code: 0 40 | Desc: The fop was completed successfully 41 | InputBucket: video 42 | InputKey: bjsp/c70f228f-5133-a2cc-a811-5dfa5433996f.mp4 43 | Pipeline: 1380710990.jcsp_bjsp 44 | Reqid: SX4AAFKzn5IZTJQU 45 | 46 | Cmd: avthumb/mp4|saveas/dmlkZW86YmpzcC9jNzBmMjI4Zi01MTMzLWEyY2MtYTgxMS01ZGZhNTQzMzk5NmYubXA0 47 | Code: 0 48 | Desc: The fop was completed successfully 49 | Hash: lrttDVJrZJYVqV46PgfJojip6Zrn 50 | Key: bjsp/c70f228f-5133-a2cc-a811-5dfa5433996f.mp4 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/privateurl.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed privateurl.md 6 | var privateUrlDocument string 7 | 8 | const PrivateUrlType = "privateurl" 9 | 10 | func init() { 11 | addCmdDocumentInfo(PrivateUrlType, privateUrlDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/privateurl.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `privateurl` 命令用来快速生成带签名的私有资源外链。七牛空间分为公开空间和私有空间,无论是公开空间还是私有空间都对应一个默认的七牛的域名,这个域名也可以是用户自己的子域名。对于公开空间的资源访问,可以直接通过拼接域名和文件名的方式访问,而对私有空间中的资源,则还需要额外的授权操作。 3 | 4 | # 格式 5 | ``` 6 | qshell privateurl [] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell privateurl -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell privateurl --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - PublicUrl:资源的公开外链 【必选】 24 | - Deadline:授权截至时间戳,单位秒 【可选】 25 | 26 | 备注: 27 | 1. `Deadline` 参数可以不指定,默认生成只有一个小时有效期的私有资源访问外链。 28 | 2. `Deadline` 参数是一个单位为秒的 Unix 时间戳,可以使用 `d2ts` 命令生成。 29 | 30 | # 示例 31 | 1 普通私有资源外链 32 | ``` 33 | $ qshell privateurl 'http://if-pri.qiniudn.com/beiyi.jpg' 34 | ``` 35 | 结果: 36 | ``` 37 | http://if-pri.qiniudn.com/beiyi.jpg?e=1427613277&token=HCALkwxJcWd_8UlXCb6QWdA-pEZj1FXXSK0G1lMr:KrDZg1MGOmntVm5Hueny8l3JNjc= 38 | ``` 39 | 40 | 2 带 `fop` 私有资源外链 41 | ``` 42 | $ qshell privateurl 'http://if-pri.qiniudn.com/beiyi.jpg?imageView2/0/w/600' 43 | ``` 44 | 结果: 45 | ``` 46 | http://if-pri.qiniudn.com/beiyi.jpg?imageView2/0/w/600&e=1427613524&token=HCALkwxJcWd_8UlXCb6QWdA-pEZj1FXXSK0G1lMr:QzpohkbhnndlKFA2-YRGieVgGPE= 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/qdownload.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed qdownload.md 6 | var qDownloadDocument string 7 | 8 | const QDownloadType = "qdownload" 9 | 10 | func init() { 11 | addCmdDocumentInfo(QDownloadType, qDownloadDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/qdownload2.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed qdownload2.md 6 | var qDownload2Document string 7 | 8 | const QDownload2Type = "qdownload2" 9 | 10 | func init() { 11 | addCmdDocumentInfo(QDownload2Type, qDownload2Document) 12 | } 13 | -------------------------------------------------------------------------------- /docs/qetag.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed qetag.md 6 | var qTagDocument string 7 | 8 | const QTagType = "qetag" 9 | 10 | func init() { 11 | addCmdDocumentInfo(QTagType, qTagDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/qetag.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `qtag` 算法是七牛用来计算文件 `hash` 的自定义算法。这个命令用来根据 `qetag` 算法快速计算文件 `hash`。 3 | 4 | 参考文档:[qetag](https://github.com/qiniu/qetag) 5 | 6 | # 格式 7 | ``` 8 | qshell qetag 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell qetag -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell qetag --doc 19 | ``` 20 | 21 | # 参数 22 | - LocalFilePath:本地文件路径 23 | 24 | # 示例 25 | ``` 26 | $ qshell qetag yyy.jpg 27 | Fu9LtwRE8Q_iy4ITrFOGYvqfbifZ 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/qshell.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2" 5 | ) 6 | 7 | const QShellType = "qshell" 8 | 9 | func init() { 10 | addCmdDocumentInfo(QShellType, qshell.ReadMeDocument) 11 | } 12 | -------------------------------------------------------------------------------- /docs/qupload.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed qupload.md 6 | var qUploadDocument string 7 | 8 | const QUploadType = "qupload" 9 | 10 | func init() { 11 | addCmdDocumentInfo(QUploadType, qUploadDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/qupload2.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed qupload2.md 6 | var qUpload2Document string 7 | 8 | const QUpload2Type = "qupload2" 9 | 10 | func init() { 11 | addCmdDocumentInfo(QUpload2Type, qUpload2Document) 12 | } 13 | -------------------------------------------------------------------------------- /docs/rename.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed rename.md 6 | var renameDocument string 7 | 8 | const RenameType = "rename" 9 | 10 | func init() { 11 | addCmdDocumentInfo(RenameType, renameDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/rename.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `rename` 命令可以对一个空间中的文件进行重命名。 3 | 注意如果目标文件已存在空间中的时候,默认情况下,`rename` 会失败,报错 `614 file exists`,如果一定要强制覆盖目标文件,可以使用选项 `--overwrite` 。 4 | 5 | 参考文档:[资源移动/重命名 (move)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/move.html) 6 | 7 | # 格式 8 | ``` 9 | qshell rename [--overwrite] 10 | ``` 11 | 12 | # 帮助文档 13 | 可以在命令行输入如下命令获取帮助文档: 14 | ``` 15 | // 简单描述 16 | $ qshell rename -h 17 | 18 | // 详细文档(此文档) 19 | $ qshell rename --doc 20 | ``` 21 | 22 | # 鉴权 23 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 24 | 25 | # 参数 26 | - Bucket: 空间名 【必须】 27 | - SrcKey: 原文件名称 【必须】 28 | - DestKey: 目标文件名称 【必须】 29 | 30 | # 选项 31 | - --overwrite: 当保存的文件已存在时,强制用新文件覆盖原文件,如果无此选项操作会失败。【可选】 32 | 33 | # 示例 34 | 1 将空间 `if-pbl` 中的 `qiniu.jpg` 重命名为 `qiniu_new.jpg` 35 | ``` 36 | qshell rename if-pbl qiniu.jpg qiniu_new.jpg 37 | ``` 38 | 39 | 2 将空间 `if-pbl` 中的 `qiniu.jpg` 重命名为 `2015/01/19/qiniu.jpg` 40 | ``` 41 | qshell rename if-pbl qiniu.jpg 2015/01/19/qiniu.jpg 42 | ``` 43 | 44 | 4 强制将空间 `if-pbl` 中的 `qiniu.jpg` 重名名为 `2015/01/19/qiniu.jpg` 45 | ``` 46 | qshell rename --overwrite if-pbl qiniu.jpg 2015/01/19/qiniu.jpg 47 | ``` 48 | 执行命令之后,此时空间 `if-pbl` 里面的 `qiniu.jpg` 文件内容覆盖空间里面原名为 `2015/01/19/qiniu.jpg`的文件,`2015/01/19/qiniu.jpg` 文件原有内容完全被`qiniu.jpg` 文件覆盖,即空间 `if-pbl` 里面的 `qiniu.jpg` 文件此后已不存在,最后剩下 `2015/01/19/qiniu.jpg` 文件,文件内容是 `qiniu.jpg` 文件的内容。可以简单理解为鸠占鹊巢。 -------------------------------------------------------------------------------- /docs/reqid.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed reqid.md 6 | var reqIdDocument string 7 | 8 | const ReqIdType = "reqid" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ReqIdType, reqIdDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/reqid.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `reqid` 命令用来解码一个七牛的自定义头部 `X-Reqid`。 3 | 4 | 七牛会给每个请求添加一个自定义的头部,这个头部是根据一定的算法生成的,解码出来其实是一个日期,有时候我们需要这个日期来查询日志。 5 | 6 | # 格式 7 | ``` 8 | qshell reqid 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell reqid -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell reqid --doc 19 | ``` 20 | 21 | # 鉴权 22 | 无 23 | 24 | # 参数 25 | - ReqId:待解码的 X-Reqid,注意是最后的一部分,比如对于 `Reqid: shared.ffmpeg.62kAAIYB06brhtsT`,这里提供的值是 `62kAAIYB06brhtsT`。 【必须】 26 | 27 | # 示例 28 | ``` 29 | $ qshell reqid 62kAAIYB06brhtsT 30 | 2015-05-06/12-14 31 | ``` -------------------------------------------------------------------------------- /docs/restorear.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed restorear.md 6 | var restoreArchiveDocument string 7 | 8 | const RestoreArchiveType = "restorear" 9 | 10 | func init() { 11 | addCmdDocumentInfo(RestoreArchiveType, restoreArchiveDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/restorear.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `restorear` 命令用来恢复一个归档文件,并且在 天之后再次恢复为原来的归档状态。 解冻有效期 1~7 天。 3 | 4 | 归档存储文件完成解冻通常需要 1~5 分钟,深度归档存储文件完成解冻需要 5~12 小时。 5 | 6 | 注:恢复仅仅是让文件可以进行下载等操作,并不会真的修改存储类型, 如果想把归档或者深度归档存储文件的存储类型转为标准存储,那么需要先将文件进行恢复(本命令),再修改文件存储类型(chtype 命令)。 7 | 8 | 参考文档:[解冻归档/深度归档存储文件](https://developer.qiniu.com/kodo/6380/restore-archive) 9 | 10 | # 格式 11 | ``` 12 | qshell restorear [flags] 13 | ``` 14 | 15 | # 帮助文档 16 | 可以在命令行输入如下命令获取帮助文档: 17 | ``` 18 | // 简单描述 19 | $ qshell reqid -h 20 | 21 | // 详细文档(此文档) 22 | $ qshell reqid --doc 23 | ``` 24 | 25 | # 鉴权 26 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 27 | 28 | # 参数 29 | - Bucket: 空间名 【必须】 30 | - Key: 源文件名称 【必须】 31 | - FreezeAfterDays: 恢复的有效期,单位:天。 【必须】 32 | 33 | # 示例 34 | ``` 35 | // 把 test 空间下的 a.txt 文件恢复,有效期为 3 天 36 | $ qshell restorear test a.txt 3 37 | ``` -------------------------------------------------------------------------------- /docs/rpcdecode.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed rpcdecode.md 6 | var rpcDecodeDocument string 7 | 8 | const RpcDecodeType = "rpcdecode" 9 | 10 | func init() { 11 | addCmdDocumentInfo(RpcDecodeType, rpcDecodeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/rpcdecode.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `rpcdecode` 命令是对通过 qiniu rpc 方式 encode 的数据进行解码。 3 | 4 | # 格式 5 | ``` 6 | qshell rpcdecode [DataToEncode...] [flags] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell rpcdecode -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell rpcdecode --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - DataToEncode:待解码的数据,当不指定则从 stdin 读取,每读取一行即进行编码并输出编码结果。 24 | 25 | # 示例 26 | ``` 27 | $ qshell rpcdecode "https:\!\!qiniu.com\!rpc'3Fa=1&b=1" 28 | https://qiniu.com/rpc?a=1&b=1 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/rpcencode.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed rpcencode.md 6 | var rpcEncodeDocument string 7 | 8 | const RpcEncodeType = "rpcencode" 9 | 10 | func init() { 11 | addCmdDocumentInfo(RpcEncodeType, rpcEncodeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/rpcencode.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `rpcencode` 命令是通过 qiniu rpc 方式对数据进行编码。 3 | 4 | # 格式 5 | ``` 6 | qshell rpcencode [ [...]] [flags] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell rpcencode -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell rpcencode --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Data:待编码的数据 【必须】 24 | 25 | # 示例 26 | ``` 27 | $ qshell rpcencode "https://qiniu.com/rpc?a=1&b=1" 28 | https:!!qiniu.com!rpc'3Fa=1&b=1 29 | ``` -------------------------------------------------------------------------------- /docs/rput.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed rput.md 6 | var rPutDocument string 7 | 8 | const RPutType = "rput" 9 | 10 | func init() { 11 | addCmdDocumentInfo(RPutType, rPutDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/saveas.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed saveas.md 6 | var saveAsDocument string 7 | 8 | const SaveAsType = "saveas" 9 | 10 | func init() { 11 | addCmdDocumentInfo(SaveAsType, saveAsDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/share-cp.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed share-cp.md 6 | var shareCpDocument string 7 | 8 | const ShareCpType = "share-cp" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ShareCpType, shareCpDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/share-cp.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `share-cp` 命令用来从目录分享链接内下载单个文件或按目录批量下载文件。 3 | 4 | # 格式 5 | ``` 6 | qshell share-cp --to= 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell share-cp -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell share-cp --doc 17 | ``` 18 | 19 | # 鉴权 20 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 21 | 22 | # 参数 23 | - Link: 分享目录链接,或是通过 `create-share` 命令的 `--output` 选项输出的文件路径【必选】 24 | 25 | # 选项 26 | - --extract-code: 提取码,只能包含六位大小写字母或者数字,如果不填写,且 `Link` 不是通过 `create-share` 命令的 `--output` 选项输出的文件路径,将会用交互模式提示输入。【可选】 27 | - --from: 指定被分享的目录或文件,如果不填写,将列举被分享的根目录。需要注意如果希望下载目录,路径必须以 `/` 结尾。【可选】 28 | - --to: 下载目标的目录路径。【必填】 29 | - -r/--recursive: 批量下载目录内所有子目录和文件 30 | 31 | # 示例 32 | ``` 33 | $ qshell share-cp -r 'http://portal.qiniu.com/kodo-shares/verify?id=AGQEKDRxBBjbGmsKduQS9oFx59rz&token=qhtbC5YmDCO-WiPriuoCG_t4hZ1LboSOtRYSJXo_%3A9uJY8FiNrKjNrt4MpBx547jlgwr8aes15z5i8VY6l5SU6ga2IKWDBSGTv1jo-rOocklE7QqApzG6okJktZ36umLoqv9x1kuo5fNmgasLXowyTuHIM3kXsaV_DoXmvQsGr5ol6j4RtrmLcKdtXhpkGH8MfSjEgRV91Bx_Q_mSwpJ1028p8yZCSad_QOu_kSPxzeLZmWlUpAtO2oEXdbMTBxhTCH_3awCgqkgoogi0FQGP4zHxeFr0n3vj69DpmWqe6DiYbYLivCuU0kOF5Khv4I6-w6vjjdY' --to=shared/ 34 | Input Extract Code: 35 | wp7gqc 36 | 37 | $ qshell share-cp share.json --from=main.go --to=shared/ 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/share-ls.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed share-ls.md 6 | var shareLsDocument string 7 | 8 | const ShareLsType = "share-ls" 9 | 10 | func init() { 11 | addCmdDocumentInfo(ShareLsType, shareLsDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/stat.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed stat.md 6 | var statDocument string 7 | 8 | const StatType = "stat" 9 | 10 | func init() { 11 | addCmdDocumentInfo(StatType, statDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/stat.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `stat` 指令根据七牛的公开API [stat](http://developer.qiniu.com/code/v6/api/kodo-api/rs/stat.html) 来获取空间中的一个文件的基本信息,包括文件的名称,保存的时间,hash值,文件的大小和MimeType。 3 | 4 | 参考文档:[资源元信息查询 (stat)](http://developer.qiniu.com/code/v6/api/kodo-api/rs/stat.html) 5 | 6 | # 格式 7 | ``` 8 | qshell stat 9 | ``` 10 | 11 | # 帮助文档 12 | 可以在命令行输入如下命令获取帮助文档: 13 | ``` 14 | // 简单描述 15 | $ qshell stat -h 16 | 17 | // 详细文档(此文档) 18 | $ qshell stat --doc 19 | ``` 20 | 21 | # 鉴权 22 | 需要使用 `qshell account` 或者 `qshell user add` 命令设置鉴权信息 `AccessKey`, `SecretKey` 和 `Name`。 23 | 24 | # 参数 25 | - Bucket:空间名,可以为公开空间或者私有空间。【必须】 26 | - Key:空间中的文件名。【必须】 27 | 28 | # 示例 29 | 获取空间 `if-pbl` 中文件 `qiniu.png` 的基本信息 30 | ``` 31 | $ qshell stat if-pbl qiniu.png 32 | ``` 33 | 34 | 输出: 35 | ``` 36 | Bucket: if-pbl 37 | Key: qiniu.png 38 | Etag: lozgLP_MAdAKZkPCXGvfd0LIDSUI 39 | MD5: 689b5cea4734143964a62214178f3f57 40 | Fsize: 5444314 -> 5.19MB 41 | PutTime: 16768889367943931 -> 2023-02-20 18:28:56.7943931 +0800 CST 42 | MimeType: text/plain 43 | Status: 未禁用 44 | RestoreStatus: 无解冻操作 45 | Expiration: 1715961600 -> 2024-05-18 00:00:00 +0800 CST 46 | TransitionToIA: 未设置 47 | TransitionToArchive: 1689609600 -> 2023-07-18 00:00:00 +0800 CST 48 | TransitionToDeepArchive: 1699977600 -> 2023-11-15 00:00:00 +0800 CST 49 | FileType: 1 -> 低频存储 50 | ``` -------------------------------------------------------------------------------- /docs/sync.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed sync.md 6 | var syncDocument string 7 | 8 | const SyncType = "sync" 9 | 10 | func init() { 11 | addCmdDocumentInfo(SyncType, syncDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/tms2d.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed tms2d.md 6 | var tms2dDocument string 7 | 8 | const TMs2dType = "tms2d" 9 | 10 | func init() { 11 | addCmdDocumentInfo(TMs2dType, tms2dDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/tms2d.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `tms2d` 该命令将一个以毫秒(ms)为单位的 Unix 时间戳转换为日期。 3 | 4 | # 格式 5 | ``` 6 | qshell tms2d 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell tms2d -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell tms2d --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - TimestampInMilliSeconds:以毫秒(ms)为单位的 Unix 时间戳。【必选】 24 | 25 | # 示例 26 | ``` 27 | $ qshell tms2d 1427252311000 28 | 2015-03-25 10:58:31 +0800 CST 29 | ``` -------------------------------------------------------------------------------- /docs/tns2d.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed tns2d.md 6 | var tns2dDocument string 7 | 8 | const TNs2dType = "tns2d" 9 | 10 | func init() { 11 | addCmdDocumentInfo(TNs2dType, tns2dDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/tns2d.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `tns2d` 因为七牛的 [stat接口](http://developer.qiniu.com/docs/v6/api/reference/rs/stat.html) 返回的 `putTime` 字段的单位是 `100纳秒`,有时候我们需要把它转出来看看。`tns2d` 这个命令就是这个作用。可以把 `putTime` 的值直接作为参数,得到日期结果。 3 | 4 | # 格式 5 | ``` 6 | qshell tns2d 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell tns2d -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell tns2d --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - TimestampIn100NanoSeconds:以 100 纳秒为单位的 Unix 时间戳。 【必选】 24 | 25 | # 示例 26 | ``` 27 | $ qshell tns2d 13603956734587420 28 | 2013-02-09 15:41:13.458742 +0800 CST 29 | ``` -------------------------------------------------------------------------------- /docs/token.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed token.md 6 | var tokenDocument string 7 | 8 | const TokenType = "token" 9 | 10 | func init() { 11 | addCmdDocumentInfo(TokenType, tokenDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/ts2d.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed ts2d.md 6 | var ts2dDocument string 7 | 8 | const TS2dType = "ts2d" 9 | 10 | func init() { 11 | addCmdDocumentInfo(TS2dType, ts2dDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/ts2d.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `ts2d` 命令将一个以秒(s)为单位的 Unix 时间戳转换为日期。 3 | 4 | # 格式 5 | ``` 6 | qshell ts2d 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell ts2d -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell ts2d --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - TimestampInSeconds:以秒(s)为单位的 Unix 时间戳。 【必须】 24 | 25 | # 示例 26 | ``` 27 | $ qshell ts2d 1427252311 28 | 2015-03-25 10:58:31 +0800 CST 29 | ``` -------------------------------------------------------------------------------- /docs/unzip.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed unzip.md 6 | var unzipDocument string 7 | 8 | const UnzipType = "unzip" 9 | 10 | func init() { 11 | addCmdDocumentInfo(UnzipType, unzipDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/unzip.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `unzip` 命令用来解压 `zip` 文件。因为七牛支持的是 UTF8 编码的文件名,Windows 自带的 zip 工具使用的是 GBK 编码的文件名。为了兼容这两种编码,所以有了 `unzip` 命令。 3 | 4 | # 格式 5 | ``` 6 | qshell unzip [] 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell unzip -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell unzip --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - QiniuZipFilePath:zip 文件路径。 【必须】 24 | 25 | # 可选 26 | - --dir:解压到指定目录,默认为命令运行的当前目录。 【可选】 27 | 28 | # 示例 29 | ``` 30 | $ qshell unzip hellp.zip /home/Temp 31 | ``` -------------------------------------------------------------------------------- /docs/urldecode.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed urldecode.md 6 | var urlDecodeDocument string 7 | 8 | const UrlDecodeType = "urldecode" 9 | 10 | func init() { 11 | addCmdDocumentInfo(UrlDecodeType, urlDecodeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/urldecode.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `urldecode` 该命令用来对 URL 编码的数据进行解码。 3 | 4 | # 格式 5 | ``` 6 | qshell urldecode 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell urldecode -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell urldecode --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - DataToDecode:待解码字符串。 【必须】 24 | 25 | # 示例 26 | ``` 27 | $ qshell urldecode %E5%A4%A7%E6%95%B0%E6%8D%AE%E6%97%B6%E4%BB%A3 28 | 大数据时代 29 | ``` -------------------------------------------------------------------------------- /docs/urlencode.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed urlencode.md 6 | var urlEncodeDocument string 7 | 8 | const UrlEncodeType = "urlencode" 9 | 10 | func init() { 11 | addCmdDocumentInfo(UrlEncodeType, urlEncodeDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/urlencode.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `urlencode` 该命令用来对一个字符串进行 URL 编码。 3 | 4 | # 格式 5 | ``` 6 | qshell urlencode 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell urlencode -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell urlencode --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | - DataToEncode:待编码的字符串。 【必须】 24 | 25 | # 示例 26 | ``` 27 | $ qshell urlencode 大数据时代 28 | %E5%A4%A7%E6%95%B0%E6%8D%AE%E6%97%B6%E4%BB%A3 29 | ``` -------------------------------------------------------------------------------- /docs/user.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed user.md 6 | var UserDetailHelpString string 7 | var User = "user" 8 | 9 | func init() { 10 | addCmdDocumentInfo(User, UserDetailHelpString) 11 | } 12 | -------------------------------------------------------------------------------- /docs/user.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `user` 命令用来对本地数据库中存储的账户信息进行管理,可以添加账号、查看/切换当前账号、列举本地保存的账号、移除特定的账号。 3 | 4 | # 格式 5 | ``` 6 | qshell user <子命令> 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell user -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell user --doc 17 | 18 | // 子命令示例 19 | $ qshell user cu -h 20 | $ qshell user cu --doc 21 | ``` 22 | 23 | # 鉴权 24 | 无 25 | 26 | 27 | # 子命令 28 | user的字命令有: 29 | * add:添加账号 30 | * clean:清除本地数据库 31 | * cu:切换当前的账户 32 | * current:查看当前账号 33 | * lookup:通过用户名字查找用户信息 34 | * ls:列出所有本地的账户信息 35 | * remove:移除特定用户 36 | 37 | # 示例 38 | 1. 添加账号 39 | ``` 40 | // --ak:七牛账号对应的 AccessKey [获取](https://portal.qiniu.com/user/key) 。【必选】 41 | // --sk:七牛账号对应的 SecretKey [获取](https://portal.qiniu.com/user/key) 。【必选】 42 | // --name:AccessKey 和 SecretKey 对的 id, 可以任意取,但同一台机器此 id 不可重复;和在七牛注册的邮箱信息没有关系, 只是 qshell 本地用来标示 对。【必选】 43 | 44 | qshell user add --ak ELUs327kxVPJrGCXqWae9yioc0xYZyrIpbM6Wh6x --sk LVzZY2SqOQ_I_kM1n00ygACVBArDvOWtiLkDtKiw --name name_test 45 | ``` 46 | 47 | 2. 清除本地数据库 48 | ``` 49 | qshell user clean // 注:仅仅清除本地数据库,会保留当前账户 50 | ``` 51 | 52 | 3. 切换当前的账户 53 | ``` 54 | qshell user cu // 切换至上一次使用的账户 55 | qshell user cu test // 切换到 `test` 账号,`test` 为 ak,sk 对的 id 56 | ``` 57 | 58 | 4. 列举本地所有的账号信息 59 | ``` 60 | qshell user ls 61 | ``` 62 | 63 | 5. 输出某个账户信息 64 | ``` 65 | qshell user lookup test // `test` 为 ak,sk 对的 id 66 | ``` 67 | 68 | 6. 删除 `test` 账号 69 | ``` 70 | qshell user remove test // `test` 为 ak,sk 对的 id 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/version.go: -------------------------------------------------------------------------------- 1 | package docs 2 | 3 | import _ "embed" 4 | 5 | //go:embed version.md 6 | var versionDocument string 7 | 8 | const VersionType = "version" 9 | 10 | func init() { 11 | addCmdDocumentInfo(VersionType, versionDocument) 12 | } 13 | -------------------------------------------------------------------------------- /docs/version.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | `version` 展示 qshell 工具当前版本信息。 3 | 4 | # 格式 5 | ``` 6 | qshell verion 7 | ``` 8 | 9 | # 帮助文档 10 | 可以在命令行输入如下命令获取帮助文档: 11 | ``` 12 | // 简单描述 13 | $ qshell verion -h 14 | 15 | // 详细文档(此文档) 16 | $ qshell verion --doc 17 | ``` 18 | 19 | # 鉴权 20 | 无 21 | 22 | # 参数 23 | 无 24 | 25 | # 示例 26 | ``` 27 | $ qshell version 28 | v2.6.2 29 | ``` -------------------------------------------------------------------------------- /iqshell/cdn/operations/qps.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import "time" 4 | 5 | var prefetchTimeTicker *time.Ticker 6 | 7 | func createQpsLimitIfNeeded(limit int) { 8 | if limit < 1 { 9 | return 10 | } 11 | 12 | if prefetchTimeTicker == nil { 13 | prefetchTimeTicker = time.NewTicker(time.Second / time.Duration(limit)) 14 | } 15 | } 16 | 17 | func waiterIfNeeded() { 18 | if prefetchTimeTicker != nil { 19 | <-prefetchTimeTicker.C 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iqshell/common/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/qshell/b0b048646b3a18eab15c89c5c4f453c4a2f10c30/iqshell/common/.DS_Store -------------------------------------------------------------------------------- /iqshell/common/account/account.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "strings" 7 | 8 | "github.com/qiniu/go-sdk/v7/auth/qbox" 9 | ) 10 | 11 | // Account - 用户自定义的账户名称 12 | type Account struct { 13 | Name string 14 | AccessKey string 15 | SecretKey string 16 | } 17 | 18 | // 获取qbox.Mac 19 | func (acc *Account) mac() (mac *qbox.Mac) { 20 | return qbox.NewMac(acc.AccessKey, acc.SecretKey) 21 | } 22 | 23 | // 对SecretKey进行加密, 保存AccessKey, 加密后的SecretKey在本地数据库中 24 | func (acc *Account) encrypt() (s string, err *data.CodeError) { 25 | encryptedKey, eErr := encryptSecretKey(acc.AccessKey, acc.SecretKey) 26 | if eErr != nil { 27 | err = eErr 28 | return 29 | } 30 | s = strings.Join([]string{acc.Name, acc.AccessKey, encryptedKey}, ":") 31 | return 32 | } 33 | 34 | // 对SecretKey加密, 形成最后的数据格式 35 | func (acc *Account) value() (v string, err *data.CodeError) { 36 | encryptedKey, eErr := encryptSecretKey(acc.AccessKey, acc.SecretKey) 37 | if eErr != nil { 38 | err = eErr 39 | return 40 | } 41 | v = encrypt(acc.AccessKey, encryptedKey, acc.Name) 42 | return 43 | } 44 | 45 | func (acc *Account) String() string { 46 | return fmt.Sprintf("Name: %s\nAccessKey: %s\nSecretKey: %s", acc.Name, acc.AccessKey, acc.SecretKey) 47 | } 48 | -------------------------------------------------------------------------------- /iqshell/common/account/load.go: -------------------------------------------------------------------------------- 1 | package account 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | 6 | "github.com/qiniu/qshell/v2/iqshell/common/log" 7 | ) 8 | 9 | type LoadInfo struct { 10 | AccountPath string 11 | OldAccountPath string 12 | AccountDBPath string 13 | } 14 | 15 | var info LoadInfo 16 | 17 | // Load 保证 AccountPath、OldAccountPath、AccountDBPath 均不为空 18 | func Load(i LoadInfo) *data.CodeError { 19 | if i.AccountDBPath == "" { 20 | return data.NewEmptyError().AppendDescF("empty account db path\n") 21 | } 22 | 23 | if i.AccountPath == "" { 24 | return data.NewEmptyError().AppendDescF("empty account path\n") 25 | } 26 | 27 | if i.OldAccountPath == "" { 28 | return data.NewEmptyError().AppendDescF("empty old account db path\n") 29 | } 30 | 31 | info = i 32 | 33 | log.Debug("account db path:" + info.AccountDBPath) 34 | log.Debug("account path:" + info.AccountPath) 35 | log.Debug("account old path:" + info.OldAccountPath) 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /iqshell/common/account/operations/add.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/account" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/common/log" 9 | ) 10 | 11 | type AddInfo struct { 12 | Name string 13 | AccessKey string 14 | SecretKey string 15 | Over bool 16 | } 17 | 18 | func (info *AddInfo) Check() *data.CodeError { 19 | if len(info.Name) == 0 { 20 | return alert.CannotEmptyError("Name", "") 21 | } 22 | if len(info.AccessKey) == 0 { 23 | return alert.CannotEmptyError("AccessKey", "") 24 | } 25 | if len(info.SecretKey) == 0 { 26 | return alert.CannotEmptyError("SecretKey", "") 27 | } 28 | return nil 29 | } 30 | 31 | // Add 保存账户信息到账户文件中, 并保存在本地数据库 32 | func Add(cfg *iqshell.Config, info AddInfo) { 33 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 34 | Checker: &info, 35 | }); !shouldContinue { 36 | return 37 | } 38 | 39 | acc := account.Account{ 40 | Name: info.Name, 41 | AccessKey: info.AccessKey, 42 | SecretKey: info.SecretKey, 43 | } 44 | 45 | if err := account.SaveToDB(acc, info.Over); err != nil { 46 | data.SetCmdStatusError() 47 | log.ErrorF("user add: save user to db error:%v", err) 48 | return 49 | } 50 | 51 | if err := account.SetAccountToLocalFile(acc); err != nil { 52 | data.SetCmdStatusError() 53 | log.ErrorF("user add: set current error:%v", err) 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /iqshell/common/account/operations/delete.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/account" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/common/log" 9 | ) 10 | 11 | type CleanInfo struct { 12 | } 13 | 14 | func (info *CleanInfo) Check() *data.CodeError { 15 | return nil 16 | } 17 | 18 | func Clean(cfg *iqshell.Config, info CleanInfo) { 19 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 20 | Checker: &info, 21 | }); !shouldContinue { 22 | return 23 | } 24 | 25 | err := account.CleanUser() 26 | if err != nil { 27 | log.Error(err) 28 | data.SetCmdStatusError() 29 | } 30 | } 31 | 32 | type RemoveInfo struct { 33 | Name string 34 | } 35 | 36 | func (info *RemoveInfo) Check() *data.CodeError { 37 | if len(info.Name) == 0 { 38 | return alert.CannotEmptyError("user name", "") 39 | } 40 | return nil 41 | } 42 | 43 | func Remove(cfg *iqshell.Config, info RemoveInfo) { 44 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 45 | Checker: &info, 46 | }); !shouldContinue { 47 | return 48 | } 49 | 50 | err := account.RmUser(info.Name) 51 | if err != nil { 52 | log.Error(err) 53 | data.SetCmdStatusError() 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /iqshell/common/account/operations/update.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/account" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | ) 9 | 10 | // ChangeInfo 切换账户 11 | type ChangeInfo struct { 12 | Name string 13 | } 14 | 15 | func (info *ChangeInfo) Check() *data.CodeError { 16 | return nil 17 | } 18 | 19 | func Change(cfg *iqshell.Config, info ChangeInfo) { 20 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 21 | Checker: &info, 22 | }); !shouldContinue { 23 | return 24 | } 25 | 26 | name, err := account.ChUser(info.Name) 27 | if err != nil { 28 | data.SetCmdStatusError() 29 | log.ErrorF("user change to %s failed, error:%v", name, err) 30 | return 31 | } else { 32 | log.InfoF("user change to %s success", name) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iqshell/common/alert/alert.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | ) 6 | 7 | func CannotEmptyError(words string, suggest string) *data.CodeError { 8 | return data.NewError(data.ErrorCodeParamNotExist, CannotEmpty(words, suggest)) 9 | } 10 | 11 | func Error(desc string, suggest string) *data.CodeError { 12 | return data.NewError(data.ErrorCodeParamNotExist, Description(desc, suggest)) 13 | } 14 | 15 | func CannotEmpty(words string, suggest string) string { 16 | desc := words 17 | if len(words) > 0 { 18 | desc += " can't be empty" 19 | } 20 | return Description(desc, suggest) 21 | } 22 | 23 | func Description(desc string, suggest string) string { 24 | ret := desc 25 | if len(suggest) > 0 { 26 | ret += ", you can do like this:\n" + suggest 27 | } 28 | return ret 29 | } 30 | -------------------------------------------------------------------------------- /iqshell/common/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/qiniu/go-sdk/v7/storage" 9 | ) 10 | 11 | var defaultClient = storage.Client{ 12 | Client: &http.Client{ 13 | Transport: &http.Transport{ 14 | Proxy: http.ProxyFromEnvironment, 15 | DialContext: (&net.Dialer{ 16 | Timeout: 20 * time.Second, 17 | KeepAlive: 20 * time.Second, 18 | }).DialContext, 19 | ForceAttemptHTTP2: true, 20 | MaxIdleConns: 2000, 21 | MaxIdleConnsPerHost: 1000, 22 | ResponseHeaderTimeout: 60 * time.Second, 23 | IdleConnTimeout: 15 * time.Second, 24 | TLSHandshakeTimeout: 15 * time.Second, 25 | ExpectContinueTimeout: 1 * time.Second, 26 | }, 27 | }, 28 | } 29 | 30 | func DefaultStorageClient() storage.Client { 31 | return defaultClient 32 | } 33 | -------------------------------------------------------------------------------- /iqshell/common/config/config_global.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/qiniu/go-sdk/v7/auth" 5 | ) 6 | 7 | func GetGlobal() *Config { 8 | return &Config{ 9 | Credentials: &auth.Credentials{ 10 | AccessKey: getAccessKey(ConfigTypeGlobal), 11 | SecretKey: []byte(getSecretKey(ConfigTypeGlobal)), 12 | }, 13 | UseHttps: getIsUseHttps(ConfigTypeGlobal), 14 | Hosts: &Hosts{ 15 | UC: GetUcHosts(ConfigTypeGlobal), 16 | Api: GetApiHosts(ConfigTypeGlobal), 17 | Rs: GetRsHosts(ConfigTypeGlobal), 18 | Rsf: GetRsfHosts(ConfigTypeGlobal), 19 | Io: GetIoHosts(ConfigTypeGlobal), 20 | Up: GetUpHosts(ConfigTypeGlobal), 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iqshell/common/config/config_user.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/qiniu/go-sdk/v7/auth" 4 | 5 | func GetUser() *Config { 6 | return &Config{ 7 | Credentials: &auth.Credentials{ 8 | AccessKey: getAccessKey(ConfigTypeUser), 9 | SecretKey: []byte(getSecretKey(ConfigTypeUser)), 10 | }, 11 | UseHttps: getIsUseHttps(ConfigTypeUser), 12 | Hosts: &Hosts{ 13 | UC: GetUcHosts(ConfigTypeUser), 14 | Api: GetApiHosts(ConfigTypeUser), 15 | Rs: GetRsHosts(ConfigTypeUser), 16 | Rsf: GetRsfHosts(ConfigTypeUser), 17 | Io: GetIoHosts(ConfigTypeUser), 18 | Up: GetUpHosts(ConfigTypeUser), 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iqshell/common/config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/log" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | type LoadInfo struct { 10 | UserConfigPath string 11 | GlobalConfigPath string 12 | } 13 | 14 | func LoadGlobalConfig(globalConfigPath string) *data.CodeError { 15 | if len(globalConfigPath) > 0 { 16 | globalConfigViper = viper.New() 17 | globalConfigViper.SetConfigFile(globalConfigPath) 18 | err := globalConfigViper.ReadInConfig() 19 | log.DebugF("read global config error:%v", err) 20 | } 21 | return nil 22 | } 23 | 24 | func LoadUserConfig(userConfigPath string) *data.CodeError { 25 | if len(userConfigPath) > 0 { 26 | userConfigViper = viper.New() 27 | userConfigViper.SetConfigFile(userConfigPath) 28 | err := userConfigViper.ReadInConfig() 29 | log.DebugF("read user config error:%v", err) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /iqshell/common/config/log.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/log" 6 | ) 7 | 8 | type LogSetting struct { 9 | LogLevel *data.String `json:"log_level,omitempty"` 10 | LogFile *data.String `json:"log_file,omitempty"` 11 | LogRotate *data.Int `json:"log_rotate,omitempty"` 12 | LogStdout *data.Bool `json:"log_stdout,omitempty"` 13 | } 14 | 15 | func (l *LogSetting) Check() *data.CodeError { 16 | if l.LogRotate == nil { 17 | l.LogRotate = data.NewInt(7) 18 | } 19 | return nil 20 | } 21 | 22 | func (l *LogSetting) Enable() bool { 23 | return l.GetLogLevel() != log.LevelNone 24 | } 25 | 26 | func (l *LogSetting) IsLogStdout() bool { 27 | if l.LogStdout == nil { 28 | return true 29 | } 30 | return l.LogStdout.Value() 31 | } 32 | 33 | func (l *LogSetting) merge(from *LogSetting) { 34 | if from == nil { 35 | return 36 | } 37 | 38 | l.LogLevel = data.GetNotEmptyStringIfExist(l.LogLevel, from.LogLevel) 39 | l.LogFile = data.GetNotEmptyStringIfExist(l.LogFile, from.LogFile) 40 | l.LogRotate = data.GetNotEmptyIntIfExist(l.LogRotate, from.LogRotate) 41 | l.LogStdout = data.GetNotEmptyBoolIfExist(l.LogStdout, from.LogStdout) 42 | } 43 | 44 | const ( 45 | NoneKey = "none" 46 | DebugKey = "debug" 47 | InfoKey = "info" 48 | WarnKey = "warn" 49 | ErrorKey = "error" 50 | ) 51 | 52 | func (l *LogSetting) GetLogLevel() (logLevel int) { 53 | if l.LogLevel == nil { 54 | return log.LevelNone 55 | } 56 | 57 | switch l.LogLevel.Value() { 58 | case DebugKey: 59 | logLevel = log.LevelDebug 60 | case InfoKey: 61 | logLevel = log.LevelInfo 62 | case WarnKey: 63 | logLevel = log.LevelWarning 64 | case ErrorKey: 65 | logLevel = log.LevelError 66 | default: 67 | logLevel = log.LevelNone 68 | } 69 | return 70 | } 71 | -------------------------------------------------------------------------------- /iqshell/common/config/operation.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "io/ioutil" 9 | "os" 10 | ) 11 | 12 | func NewConfigWithPath(path string) (*Config, *data.CodeError) { 13 | file, err := os.Open(path) 14 | if err != nil || file == nil { 15 | return nil, data.NewEmptyError().AppendError(err) 16 | } 17 | 18 | defer func(file *os.File) { 19 | file.Close() 20 | }(file) 21 | 22 | configData, err := ioutil.ReadAll(file) 23 | if err != nil || configData == nil { 24 | return nil, data.NewEmptyError().AppendError(err) 25 | } 26 | 27 | config := &Config{} 28 | if e := json.Unmarshal(configData, &config); e != nil { 29 | return nil, data.NewEmptyError().AppendError(err) 30 | } 31 | return config, nil 32 | } 33 | 34 | func (c *Config) UpdateToLocal(path string) *data.CodeError { 35 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0666) 36 | if err != nil || file == nil { 37 | return data.NewEmptyError().AppendError(err) 38 | } 39 | 40 | defer func(file *os.File) { 41 | file.Close() 42 | }(file) 43 | 44 | configData, err := json.MarshalIndent(c, "", "\t") 45 | if err != nil || configData == nil || len(configData) == 0 { 46 | return data.ConvertError(err) 47 | } 48 | 49 | fmt.Println("configData:" + string(configData)) 50 | writer := bufio.NewWriter(file) 51 | 52 | _, err = writer.Write(configData) 53 | if err != nil { 54 | return data.NewEmptyError().AppendError(err) 55 | } 56 | 57 | err = writer.Flush() 58 | 59 | return data.ConvertError(err) 60 | } 61 | -------------------------------------------------------------------------------- /iqshell/common/config/retry.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | ) 6 | 7 | type Retry struct { 8 | Max *data.Int `json:"max,omitempty"` 9 | Interval *data.Int `json:"interval,omitempty"` 10 | } 11 | 12 | func (r *Retry) merge(from *Retry) { 13 | if from == nil { 14 | return 15 | } 16 | 17 | r.Max = data.GetNotEmptyIntIfExist(r.Max, from.Max) 18 | r.Interval = data.GetNotEmptyIntIfExist(r.Interval, from.Interval) 19 | } 20 | -------------------------------------------------------------------------------- /iqshell/common/config/tasks.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | ) 6 | 7 | type Tasks struct { 8 | ConcurrentCount *data.Int `json:"concurrent_count,omitempty"` 9 | StopWhenOneTaskFailed *data.Bool `json:"stop_when_one_task_failed,omitempty"` 10 | } 11 | 12 | func (t *Tasks) merge(from *Tasks) { 13 | if from == nil { 14 | return 15 | } 16 | 17 | t.ConcurrentCount = data.GetNotEmptyIntIfExist(t.ConcurrentCount, from.ConcurrentCount) 18 | t.StopWhenOneTaskFailed = data.GetNotEmptyBoolIfExist(t.StopWhenOneTaskFailed, from.StopWhenOneTaskFailed) 19 | } 20 | -------------------------------------------------------------------------------- /iqshell/common/config/viper.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/spf13/cast" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | func getStringValueFromLocal(vipers []*viper.Viper, localKey []string) *data.String { 10 | var value *data.String 11 | getValueFromLocal(vipers, localKey, func(v interface{}) (stop bool) { 12 | if v == nil { 13 | return false 14 | } 15 | value = data.NewString(cast.ToString(v)) 16 | return true 17 | }) 18 | return value 19 | } 20 | 21 | func getStringArrayValueFromLocal(vipers []*viper.Viper, localKey []string) []string { 22 | var value []string 23 | getValueFromLocal(vipers, localKey, func(v interface{}) (stop bool) { 24 | if v == nil { 25 | return false 26 | } 27 | value = cast.ToStringSlice(v) 28 | return true 29 | }) 30 | return value 31 | } 32 | 33 | func getBoolValueFromLocal(vipers []*viper.Viper, localKey []string) *data.Bool { 34 | var value *data.Bool 35 | getValueFromLocal(vipers, localKey, func(v interface{}) (stop bool) { 36 | if v == nil { 37 | return false 38 | } 39 | value = data.NewBool(cast.ToBool(v)) 40 | return true 41 | }) 42 | return value 43 | } 44 | 45 | func getValueFromLocal(vipers []*viper.Viper, localKey []string, f func(value interface{}) (stop bool)) { 46 | if f == nil { 47 | return 48 | } 49 | 50 | var stop = false 51 | var value interface{} 52 | for _, key := range localKey { 53 | for _, vip := range vipers { 54 | if value = vip.Get(key); value != nil { 55 | if stop = f(value); stop { 56 | break 57 | } 58 | } 59 | } 60 | 61 | if stop { 62 | break 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /iqshell/common/data/checker.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | type Checker interface { 4 | Check() *CodeError 5 | } 6 | -------------------------------------------------------------------------------- /iqshell/common/data/data_test.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type Data struct { 10 | Can *Bool `json:"can"` 11 | Age *Int `json:"age"` 12 | Name *String `json:"name"` 13 | } 14 | 15 | func TestData(t *testing.T) { 16 | 17 | d := Data{ 18 | Can: NewBool(true), 19 | Age: NewInt(10), 20 | Name: NewString("tom"), 21 | } 22 | 23 | dataString := "" 24 | data, err := json.Marshal(&d) 25 | if err != nil { 26 | t.Fail() 27 | } else { 28 | dataString = string(data) 29 | } 30 | 31 | fmt.Println("marshal json:" + dataString) 32 | 33 | d = Data{} 34 | err = json.Unmarshal(data, &d) 35 | if err != nil { 36 | t.Fail() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iqshell/common/data/define.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | const ( 4 | TrueString = "true" 5 | FalseString = "false" 6 | 7 | ResumeApiV1 = "v1" 8 | ResumeApiV2 = "v2" 9 | 10 | DefaultLineSeparate = "\t" 11 | 12 | BLOCK_BITS = 22 // Indicate that the blocksize is 4M 13 | BLOCK_SIZE = 1 << BLOCK_BITS // BLOCK SIZE 14 | ) 15 | -------------------------------------------------------------------------------- /iqshell/common/data/env.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "os" 4 | 5 | const ( 6 | EnvKeyTest = "qshell_test" 7 | ) 8 | 9 | func IsTestMode() bool { 10 | return os.Getenv(EnvKeyTest) == TrueString 11 | } 12 | 13 | func SetTestMode() *CodeError { 14 | return ConvertError(os.Setenv(EnvKeyTest, TrueString)) 15 | } 16 | -------------------------------------------------------------------------------- /iqshell/common/data/global_var.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import "sync" 4 | 5 | // 此处需要明确出值 6 | const ( 7 | StatusOK = 0 // process success 8 | StatusError = 1 // process error 9 | StatusUserCancel = 2 // 用户取消 10 | ) 11 | 12 | var ( 13 | mu sync.Mutex 14 | cmdStatus = StatusOK 15 | ) 16 | 17 | func SetCmdStatus(status int) { 18 | mu.Lock() 19 | defer mu.Unlock() 20 | if cmdStatus == StatusUserCancel { 21 | return 22 | } 23 | 24 | cmdStatus = status 25 | } 26 | 27 | func SetCmdStatusError() { 28 | SetCmdStatus(StatusError) 29 | } 30 | 31 | func SetCmdStatusUserCancel() { 32 | SetCmdStatus(StatusUserCancel) 33 | } 34 | 35 | func GetCmdStatus() int { 36 | mu.Lock() 37 | defer mu.Unlock() 38 | 39 | return cmdStatus 40 | } 41 | -------------------------------------------------------------------------------- /iqshell/common/data/std.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "io" 5 | "os" 6 | ) 7 | 8 | var stdout io.WriteCloser = os.Stdout 9 | var stderr io.WriteCloser = os.Stderr 10 | 11 | func Stdout() io.WriteCloser { 12 | return stdout 13 | } 14 | 15 | func Stderr() io.WriteCloser { 16 | return stderr 17 | } 18 | 19 | func SetStdout(o io.WriteCloser) { 20 | stdout = o 21 | } 22 | 23 | func SetStderr(e io.WriteCloser) { 24 | stderr = e 25 | } 26 | -------------------------------------------------------------------------------- /iqshell/common/export/export.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | type Exporter interface { 12 | Export(a ...interface{}) 13 | ExportF(format string, a ...interface{}) 14 | Close() *data.CodeError 15 | } 16 | 17 | func New(file string) (Exporter, *data.CodeError) { 18 | if len(file) == 0 { 19 | return empty(), nil 20 | } 21 | 22 | fileHandler, err := os.Create(file) 23 | if err != nil { 24 | err = data.NewEmptyError().AppendDescF("open file: %s: %v\n", file, err) 25 | return empty(), data.NewEmptyError().AppendDesc("open file:" + file).AppendError(err) 26 | } 27 | 28 | return &exporter{ 29 | file: fileHandler, 30 | lock: sync.RWMutex{}, 31 | writer: bufio.NewWriter(fileHandler), 32 | }, nil 33 | } 34 | 35 | func empty() Exporter { 36 | return &exporter{} 37 | } 38 | 39 | type exporter struct { 40 | file *os.File 41 | lock sync.RWMutex 42 | writer *bufio.Writer 43 | } 44 | 45 | var _ Exporter = (*exporter)(nil) 46 | 47 | func (e *exporter) Close() *data.CodeError { 48 | if e == nil || e.file == nil { 49 | return nil 50 | } 51 | 52 | if err := e.file.Close(); err != nil { 53 | return data.NewEmptyError().AppendError(err) 54 | } 55 | return nil 56 | } 57 | 58 | func (e *exporter) Export(a ...interface{}) { 59 | e.export(fmt.Sprint(a...)) 60 | } 61 | 62 | func (e *exporter) ExportF(format string, a ...interface{}) { 63 | e.export(fmt.Sprintf(format, a...)) 64 | } 65 | 66 | func (e *exporter) export(text string) { 67 | if e != nil && e.writer != nil { 68 | e.lock.Lock() 69 | e.writer.WriteString(text + "\n") 70 | e.writer.Flush() 71 | e.lock.Unlock() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /iqshell/common/flow/code_verification.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/common/log" 7 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 8 | ) 9 | 10 | func UserCodeVerification() (success bool) { 11 | code := utils.CreateRandString(6) 12 | log.Warning(fmt.Sprintf(" Input %s to confirm operation: ", code)) 13 | 14 | confirm := "" 15 | _, err := fmt.Scanln(&confirm) 16 | if err != nil { 17 | _, _ = fmt.Fprintf(data.Stdout(), "scan error:%v\n", err) 18 | return false 19 | } 20 | 21 | if code != confirm { 22 | _, _ = fmt.Fprintln(data.Stdout(), "Task quit!") 23 | return false 24 | } 25 | 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /iqshell/common/flow/event_listener.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type EventListener struct { 6 | FlowWillStartFunc func(flow *Flow) (err *data.CodeError) 7 | FlowWillEndFunc func(flow *Flow) (err *data.CodeError) 8 | WillWorkFunc func(work *WorkInfo) (shouldContinue bool, err *data.CodeError) 9 | OnWorkSkipFunc func(work *WorkInfo, result Result, err *data.CodeError) 10 | OnWorkSuccessFunc func(work *WorkInfo, result Result) 11 | OnWorkFailFunc func(work *WorkInfo, err *data.CodeError) 12 | } 13 | 14 | func (e *EventListener) FlowWillStart(flow *Flow) (err *data.CodeError) { 15 | if e.FlowWillStartFunc == nil { 16 | return nil 17 | } 18 | return e.FlowWillStartFunc(flow) 19 | } 20 | 21 | func (e *EventListener) FlowWillEnd(flow *Flow) (err *data.CodeError) { 22 | if e.FlowWillEndFunc == nil { 23 | return nil 24 | } 25 | return e.FlowWillEndFunc(flow) 26 | } 27 | 28 | func (e *EventListener) WillWork(work *WorkInfo) (shouldContinue bool, err *data.CodeError) { 29 | if e.WillWorkFunc == nil { 30 | return true, nil 31 | } 32 | return e.WillWorkFunc(work) 33 | } 34 | 35 | func (e *EventListener) OnWorkSkip(work *WorkInfo, result Result, err *data.CodeError) { 36 | if e.OnWorkSkipFunc == nil { 37 | return 38 | } 39 | e.OnWorkSkipFunc(work, result, err) 40 | } 41 | 42 | func (e *EventListener) OnWorkSuccess(work *WorkInfo, result Result) { 43 | if e.OnWorkSuccessFunc == nil { 44 | return 45 | } 46 | e.OnWorkSuccessFunc(work, result) 47 | } 48 | 49 | func (e *EventListener) OnWorkFail(work *WorkInfo, err *data.CodeError) { 50 | if e.OnWorkFailFunc == nil { 51 | return 52 | } 53 | e.OnWorkFailFunc(work, err) 54 | } 55 | -------------------------------------------------------------------------------- /iqshell/common/flow/overseer.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type Overseer interface { 6 | WillWork(work *WorkInfo) 7 | WorkDone(record *WorkRecord) 8 | GetWorkRecordIfHasDone(work *WorkInfo) (hasDone bool, record *WorkRecord) 9 | } 10 | 11 | type WorkRecord struct { 12 | *WorkInfo 13 | 14 | Result Result `json:"result"` 15 | Err *data.CodeError `json:"err"` 16 | } 17 | -------------------------------------------------------------------------------- /iqshell/common/flow/redo.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type Redo interface { 6 | 7 | // ShouldRedo 8 | // @Description: 是否需要重新做 9 | // @param work 工作信息 10 | // @param workRecord 此工作的记录 11 | // @return shouldRedo 是否需要重做 12 | // @return cause 需要重做或不能重做的原因 13 | ShouldRedo(work *WorkInfo, workRecord *WorkRecord) (shouldRedo bool, cause *data.CodeError) 14 | } 15 | 16 | func NewRedo(f func(work *WorkInfo, workRecord *WorkRecord) (shouldRedo bool, cause *data.CodeError)) Redo { 17 | return &redo{f: f} 18 | } 19 | 20 | type redo struct { 21 | f func(work *WorkInfo, workRecord *WorkRecord) (shouldRedo bool, cause *data.CodeError) 22 | } 23 | 24 | func (r *redo) ShouldRedo(work *WorkInfo, workRecord *WorkRecord) (shouldRedo bool, cause *data.CodeError) { 25 | if r.f == nil { 26 | return false, nil 27 | } 28 | return r.f(work, workRecord) 29 | } 30 | -------------------------------------------------------------------------------- /iqshell/common/flow/result.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | type Result interface { 4 | IsValid() bool 5 | } 6 | -------------------------------------------------------------------------------- /iqshell/common/flow/skip.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type Skipper interface { 6 | ShouldSkip(work *WorkInfo) (skip bool, cause *data.CodeError) 7 | } 8 | 9 | func NewSkipper(f func(work *WorkInfo) (skip bool, cause *data.CodeError)) Skipper { 10 | return &skipper{f: f} 11 | } 12 | 13 | type skipper struct { 14 | f func(work *WorkInfo) (skip bool, cause *data.CodeError) 15 | } 16 | 17 | func (s *skipper) ShouldSkip(work *WorkInfo) (skip bool, cause *data.CodeError) { 18 | if s.f == nil { 19 | return false, nil 20 | } 21 | return s.f(work) 22 | } 23 | -------------------------------------------------------------------------------- /iqshell/common/flow/work.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | type Work interface { 4 | WorkId() string 5 | } 6 | 7 | type WorkInfo struct { 8 | Data string `json:"data"` 9 | Work Work `json:"work"` 10 | } 11 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_creator.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | const ErrorSeparate = "\tQShellError:" 6 | 7 | type WorkCreator interface { 8 | Create(info string) (work Work, err *data.CodeError) 9 | } 10 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_creator_items.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 7 | ) 8 | 9 | const DefaultLineItemSeparate = "\t" 10 | 11 | type itemsWorkCreator struct { 12 | separate string 13 | minItemsCount int 14 | creatorFunc func(items []string) (work Work, err *data.CodeError) 15 | } 16 | 17 | func (l *itemsWorkCreator) Create(info string) (work Work, err *data.CodeError) { 18 | items := utils.SplitString(info, l.separate) 19 | if len(info) > 0 && len(items) >= l.minItemsCount { 20 | return l.creatorFunc(items) 21 | } 22 | return nil, data.NewError(data.ErrorCodeParamMissing, fmt.Sprintf("at least %d parameter is required", l.minItemsCount)) 23 | } 24 | 25 | func NewItemsWorkCreator(separate string, minItemsCount int, creatorFunc func(items []string) (work Work, err *data.CodeError)) WorkCreator { 26 | if len(separate) == 0 { 27 | separate = DefaultLineItemSeparate 28 | } 29 | return &itemsWorkCreator{ 30 | separate: separate, 31 | minItemsCount: minItemsCount, 32 | creatorFunc: creatorFunc, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_creator_json.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | ) 7 | 8 | type jsonWorkCreator struct { 9 | BlankQuotedWorkCreatFunc func() Work 10 | } 11 | 12 | func (w *jsonWorkCreator) Create(info string) (Work, *data.CodeError) { 13 | work := w.BlankQuotedWorkCreatFunc() 14 | err := json.Unmarshal([]byte(info), work) 15 | return work, data.ConvertError(err) 16 | } 17 | 18 | func NewJsonWorkCreator(blankQuotedWorkCreatFunc func() Work) WorkCreator { 19 | return &jsonWorkCreator{ 20 | BlankQuotedWorkCreatFunc: blankQuotedWorkCreatFunc, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_provider.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/common/log" 7 | "os" 8 | ) 9 | 10 | const UnknownWorkCount = int64(-1) 11 | 12 | type WorkProvider interface { 13 | WorkTotalCount() int64 14 | Provide() (hasMore bool, work *WorkInfo, err *data.CodeError) 15 | } 16 | 17 | func NewWorkProviderOfFile(filepath string, enableStdin bool, creator WorkCreator) (provider WorkProvider, err *data.CodeError) { 18 | if len(filepath) > 0 { 19 | return NewFileWorkProvider(filepath, creator) 20 | } 21 | 22 | if enableStdin { 23 | log.InfoF("input info with stdin, you can end the input with Ctrl-D or cancel the task with Ctrl-C") 24 | return NewReaderWorkProvider(os.Stdin, creator) 25 | } 26 | 27 | return nil, alert.CannotEmptyError("FilePath (WorkProviderOfFile)", "") 28 | } 29 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_provider_array.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "sync" 7 | ) 8 | 9 | func NewArrayWorkProvider(works []Work) (WorkProvider, *data.CodeError) { 10 | if works == nil { 11 | return nil, alert.CannotEmptyError("works (ArrayWorkProvider)", "") 12 | } 13 | 14 | return &arrayWorkProvider{ 15 | readOffset: 0, 16 | works: createWorkInfoListWithWorkList(works), 17 | }, nil 18 | } 19 | 20 | type arrayWorkProvider struct { 21 | mu sync.Mutex 22 | readOffset int 23 | works []*WorkInfo 24 | } 25 | 26 | func (p *arrayWorkProvider) WorkTotalCount() int64 { 27 | return int64(len(p.works)) 28 | } 29 | 30 | func (p *arrayWorkProvider) Provide() (hasMore bool, work *WorkInfo, err *data.CodeError) { 31 | p.mu.Lock() 32 | hasMore, work, err = p.provide() 33 | p.mu.Unlock() 34 | return 35 | } 36 | 37 | func (p *arrayWorkProvider) provide() (hasMore bool, work *WorkInfo, err *data.CodeError) { 38 | if p.readOffset > len(p.works)-1 { 39 | return false, &WorkInfo{}, nil 40 | } 41 | hasMore = true 42 | work = p.works[p.readOffset] 43 | p.readOffset++ 44 | return 45 | } 46 | 47 | func createWorkInfoListWithWorkList(works []Work) []*WorkInfo { 48 | if works == nil { 49 | return nil 50 | } 51 | 52 | infos := make([]*WorkInfo, 0, len(works)) 53 | for _, work := range works { 54 | infos = append(infos, &WorkInfo{ 55 | Data: "", 56 | Work: work, 57 | }) 58 | } 59 | return infos 60 | } 61 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_provider_chan.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | ) 8 | 9 | func NewChanWorkProvider(works <-chan Work) (WorkProvider, *data.CodeError) { 10 | if works == nil { 11 | return nil, alert.CannotEmptyError("works (ChanWorkProvider)", "") 12 | } 13 | 14 | return &chanWorkProvider{ 15 | works: works, 16 | }, nil 17 | } 18 | 19 | type chanWorkProvider struct { 20 | works <-chan Work 21 | } 22 | 23 | func (p *chanWorkProvider) WorkTotalCount() int64 { 24 | return -1 25 | } 26 | 27 | func (p *chanWorkProvider) Provide() (hasMore bool, work *WorkInfo, err *data.CodeError) { 28 | for w := range p.works { 29 | return true, &WorkInfo{ 30 | Data: fmt.Sprintf("%+v", w), 31 | Work: w, 32 | }, nil 33 | } 34 | return false, &WorkInfo{}, nil 35 | } 36 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_provider_file.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 6 | "os" 7 | ) 8 | 9 | func NewFileWorkProvider(filePath string, creator WorkCreator) (WorkProvider, *data.CodeError) { 10 | f, oErr := os.Open(filePath) 11 | if oErr != nil { 12 | return nil, data.NewEmptyError().AppendDescF("FileWorkProvider, open file error:%v", oErr) 13 | } 14 | 15 | provider, rErr := NewReaderWorkProvider(f, creator) 16 | if rErr != nil { 17 | return nil, data.ConvertError(rErr) 18 | } 19 | 20 | workCount, fErr := utils.FileLineCounts(filePath) 21 | if fErr != nil { 22 | workCount = UnknownWorkCount 23 | } 24 | 25 | return &fileWorkProvider{ 26 | workCount: workCount, 27 | workProvider: provider, 28 | }, nil 29 | } 30 | 31 | type fileWorkProvider struct { 32 | workCount int64 33 | workProvider WorkProvider 34 | } 35 | 36 | func (p *fileWorkProvider) WorkTotalCount() int64 { 37 | return p.workCount 38 | } 39 | 40 | func (p *fileWorkProvider) Provide() (hasMore bool, work *WorkInfo, err *data.CodeError) { 41 | return p.workProvider.Provide() 42 | } 43 | -------------------------------------------------------------------------------- /iqshell/common/flow/work_provider_reader.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "bufio" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "io" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | func NewReaderWorkProvider(reader io.Reader, creator WorkCreator) (WorkProvider, *data.CodeError) { 13 | if reader == nil { 14 | return nil, alert.CannotEmptyError("work reader (ReaderWorkProvider)", "") 15 | } 16 | if creator == nil { 17 | return nil, alert.CannotEmptyError("work creator (ReaderWorkProvider)", "") 18 | } 19 | return &readerWorkProvider{ 20 | scanner: bufio.NewScanner(reader), 21 | creator: creator, 22 | }, nil 23 | } 24 | 25 | type readerWorkProvider struct { 26 | mu sync.Mutex 27 | scanner *bufio.Scanner 28 | creator WorkCreator 29 | } 30 | 31 | func (p *readerWorkProvider) WorkTotalCount() int64 { 32 | return UnknownWorkCount 33 | } 34 | 35 | func (p *readerWorkProvider) Provide() (hasMore bool, work *WorkInfo, err *data.CodeError) { 36 | p.mu.Lock() 37 | hasMore, work, err = p.provide() 38 | p.mu.Unlock() 39 | return 40 | } 41 | 42 | func (p *readerWorkProvider) provide() (hasMore bool, work *WorkInfo, err *data.CodeError) { 43 | success := p.scanner.Scan() 44 | if success { 45 | line := p.scanner.Text() 46 | if items := strings.Split(line, ErrorSeparate); len(items) > 0 { 47 | line = items[0] 48 | } 49 | w, e := p.creator.Create(line) 50 | return true, &WorkInfo{ 51 | Data: line, 52 | Work: w, 53 | }, e 54 | } 55 | return false, &WorkInfo{}, nil 56 | } 57 | -------------------------------------------------------------------------------- /iqshell/common/flow/worker.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | ) 7 | 8 | type Worker interface { 9 | 10 | // DoWork 处理工作 11 | // @Description: recordList 长度需和 workInfos 长度想等 12 | // @param workInfos 工作列表 13 | // @return recordList 工作记录列表 14 | // @return err 工作错误信息 15 | DoWork(workInfos []*WorkInfo) (recordList []*WorkRecord, err *data.CodeError) 16 | } 17 | 18 | func NewWorker(doFunc func(workInfos []*WorkInfo) ([]*WorkRecord, *data.CodeError)) Worker { 19 | return &workerStruct{ 20 | DoFunc: doFunc, 21 | } 22 | } 23 | 24 | func NewSimpleWorker(doFunc func(workInfo *WorkInfo) (Result, *data.CodeError)) Worker { 25 | return &workerStruct{ 26 | SimpleDoFunc: doFunc, 27 | } 28 | } 29 | 30 | type workerStruct struct { 31 | SimpleDoFunc func(workInfo *WorkInfo) (Result, *data.CodeError) 32 | DoFunc func(workInfos []*WorkInfo) ([]*WorkRecord, *data.CodeError) 33 | } 34 | 35 | func (w *workerStruct) DoWork(workInfoList []*WorkInfo) ([]*WorkRecord, *data.CodeError) { 36 | if w == nil { 37 | return nil, alert.Error("worker: no worker", "") 38 | } 39 | 40 | if w.DoFunc != nil { 41 | return w.DoFunc(workInfoList) 42 | } 43 | 44 | if w.SimpleDoFunc != nil { 45 | recordList := make([]*WorkRecord, 0, len(workInfoList)) 46 | for _, workInfo := range workInfoList { 47 | record := &WorkRecord{ 48 | WorkInfo: workInfo, 49 | } 50 | record.Result, record.Err = w.SimpleDoFunc(workInfo) 51 | recordList = append(recordList, record) 52 | } 53 | return recordList, nil 54 | } 55 | 56 | return nil, alert.Error("worker: no worker func", "") 57 | } 58 | -------------------------------------------------------------------------------- /iqshell/common/flow/worker_provider.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | ) 7 | 8 | type WorkerProvider interface { 9 | Provide() (worker Worker, err *data.CodeError) 10 | } 11 | 12 | func NewWorkerProvider(builder func() (Worker, *data.CodeError)) WorkerProvider { 13 | return &workerProvider{ 14 | Builder: builder, 15 | } 16 | } 17 | 18 | type workerProvider struct { 19 | Builder func() (Worker, *data.CodeError) 20 | } 21 | 22 | func (w *workerProvider) Provide() (Worker, *data.CodeError) { 23 | if w == nil || w.Builder == nil { 24 | return nil, alert.Error("worker: no workerProvider Builder", "") 25 | } 26 | return w.Builder() 27 | } 28 | -------------------------------------------------------------------------------- /iqshell/common/host/host.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/provider" 4 | 5 | type Host struct { 6 | Host string 7 | Domain string // 可为 host,也可为 IP + 端口 8 | } 9 | 10 | var _ provider.Item = (*Host)(nil) 11 | 12 | func (h *Host) Equal(item provider.Item) bool { 13 | host, _ := item.(*Host) 14 | if h == nil || host == nil { 15 | return false 16 | } 17 | return h.Host == host.Host && h.Domain == host.Domain 18 | } 19 | 20 | func (h *Host) GetServer() string { 21 | if len(h.Domain) > 0 { 22 | return h.Domain 23 | } 24 | return h.Host 25 | } 26 | 27 | func (h *Host) GetHost() string { 28 | return h.Host 29 | } 30 | -------------------------------------------------------------------------------- /iqshell/common/host/provider.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/provider" 6 | ) 7 | 8 | type Provider interface { 9 | Available() (available bool, err *data.CodeError) 10 | Provide() (host *Host, err *data.CodeError) 11 | Freeze(host *Host) 12 | } 13 | 14 | func NewListProviderWithHostStrings(hostStrings []string) Provider { 15 | hosts := make([]*Host, 0, len(hostStrings)) 16 | for _, h := range hostStrings { 17 | hosts = append(hosts, &Host{ 18 | Host: h, 19 | Domain: "", 20 | }) 21 | } 22 | return NewListProvider(hosts) 23 | } 24 | 25 | func NewListProvider(hosts []*Host) Provider { 26 | items := make([]provider.Item, 0, len(hosts)) 27 | for _, h := range hosts { 28 | items = append(items, h) 29 | } 30 | return &listProvider{ 31 | p: provider.NewListProvider(items), 32 | } 33 | } 34 | 35 | type listProvider struct { 36 | p provider.Provider 37 | } 38 | 39 | func (l *listProvider) Available() (available bool, err *data.CodeError) { 40 | return l.p.Available() 41 | } 42 | 43 | func (l *listProvider) Provide() (host *Host, err *data.CodeError) { 44 | i, e := l.p.Provide() 45 | host, _ = i.(*Host) 46 | return host, e 47 | } 48 | 49 | func (l *listProvider) Freeze(host *Host) { 50 | l.p.Freeze(host) 51 | } 52 | -------------------------------------------------------------------------------- /iqshell/common/limit/limit.go: -------------------------------------------------------------------------------- 1 | package limit 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type Limit interface { 6 | Acquire(count int) *data.CodeError 7 | Release(count int) 8 | } 9 | -------------------------------------------------------------------------------- /iqshell/common/log/data.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/astaxie/beego/logs" 6 | ) 7 | 8 | const ( 9 | LevelAlert Level = logs.LevelAlert 10 | LevelError Level = logs.LevelError 11 | LevelWarning Level = logs.LevelWarning 12 | LevelInfo Level = logs.LevelInformational 13 | LevelDebug Level = logs.LevelDebug 14 | LevelNone Level = 100000 // 不输出日志 15 | ) 16 | 17 | type Level = int 18 | 19 | type Config struct { 20 | Filename string `json:"filename"` 21 | Level int `json:"level"` 22 | Daily bool `json:"daily"` 23 | MaxDays int `json:"maxdays"` 24 | StdOutColorful bool `json:"color"` 25 | EnableStdout bool `json:"-"` 26 | } 27 | 28 | func (c *Config) ToJson() string { 29 | cfgBytes, _ := json.Marshal(c) 30 | return string(cfgBytes) 31 | } 32 | -------------------------------------------------------------------------------- /iqshell/common/log/load.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/astaxie/beego/logs" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | ) 7 | 8 | func Prepare() *data.CodeError { 9 | progressLog = new(logs.BeeLogger) 10 | return nil 11 | } 12 | 13 | func LoadConsole(cfg Config) (err *data.CodeError) { 14 | if e := progressLog.SetLogger(adapterConsole, cfg.ToJson()); e != nil { 15 | return data.NewEmptyError().AppendDesc("load console error when set logger").AppendError(e) 16 | } 17 | 18 | // 日志总开关 19 | progressLog.SetLevel(LevelDebug) 20 | 21 | if e := progressLog.DelLogger(logs.AdapterConsole); e != nil { 22 | return data.NewEmptyError().AppendDesc("load console error when del logger").AppendError(e) 23 | } 24 | 25 | return 26 | } 27 | 28 | func LoadFileLogger(cfg Config) (err *data.CodeError) { 29 | if len(cfg.Filename) > 0 { 30 | if e := progressLog.SetLogger(logs.AdapterFile, cfg.ToJson()); e != nil { 31 | return data.NewEmptyError().AppendDesc("set file logger").AppendError(e) 32 | } 33 | } 34 | 35 | if !cfg.EnableStdout { 36 | if dErr := progressLog.DelLogger(adapterConsole); dErr != nil { 37 | WarningF("disable stdout error:%v", dErr) 38 | } 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /iqshell/common/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "github.com/astaxie/beego/logs" 6 | ) 7 | 8 | var progressLog *logs.BeeLogger 9 | 10 | func Debug(a ...interface{}) { 11 | DebugF(fmt.Sprint(a...)) 12 | } 13 | 14 | func DebugF(format string, v ...interface{}) { 15 | if progressLog != nil { 16 | progressLog.Debug(format, v...) 17 | } else { 18 | fmt.Printf(format, v...) 19 | fmt.Println("") 20 | } 21 | } 22 | 23 | func Info(a ...interface{}) { 24 | InfoF(fmt.Sprint(a...)) 25 | } 26 | 27 | func InfoF(format string, v ...interface{}) { 28 | if progressLog != nil { 29 | progressLog.Info(format, v...) 30 | } else { 31 | fmt.Printf(format, v...) 32 | fmt.Println("") 33 | } 34 | } 35 | 36 | func Warning(a ...interface{}) { 37 | WarningF(fmt.Sprint(a...)) 38 | } 39 | 40 | func WarningF(format string, v ...interface{}) { 41 | if progressLog != nil { 42 | progressLog.Warn(format, v...) 43 | } else { 44 | fmt.Printf(format, v...) 45 | fmt.Println("") 46 | } 47 | } 48 | 49 | func Error(a ...interface{}) { 50 | ErrorF(fmt.Sprint(a...)) 51 | } 52 | 53 | func ErrorF(format string, v ...interface{}) { 54 | if progressLog != nil { 55 | progressLog.Error(format, v...) 56 | } else { 57 | fmt.Printf(format, v...) 58 | fmt.Println("") 59 | } 60 | } 61 | 62 | func Alert(a ...interface{}) { 63 | AlertF(fmt.Sprint(a...)) 64 | } 65 | 66 | func AlertF(format string, v ...interface{}) { 67 | if progressLog != nil { 68 | progressLog.Alert(format, v...) 69 | } else { 70 | fmt.Printf(format, v...) 71 | fmt.Println("") 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /iqshell/common/progress/progress.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import "io" 4 | 5 | type Progress interface { 6 | io.Writer 7 | 8 | Start() 9 | SetFileSize(fileSize int64) 10 | SendSize(newSize int64) 11 | Progress(current int64) 12 | End() 13 | } 14 | -------------------------------------------------------------------------------- /iqshell/common/provider/data.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | type Item interface { 4 | Equal(item Item) bool 5 | } 6 | -------------------------------------------------------------------------------- /iqshell/common/recorder/recorder.go: -------------------------------------------------------------------------------- 1 | package recorder 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type Recorder interface { 6 | 7 | // Get 获取记录 8 | Get(key string) (value string, err *data.CodeError) 9 | 10 | // Put 添加记录 11 | Put(key, value string) *data.CodeError 12 | 13 | // Delete 删除记录 14 | Delete(key string) *data.CodeError 15 | } 16 | -------------------------------------------------------------------------------- /iqshell/common/synchronized/sync.go: -------------------------------------------------------------------------------- 1 | package synchronized 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "sync" 6 | ) 7 | 8 | type Locker interface { 9 | Lock() 10 | Unlock() 11 | } 12 | 13 | type Synchronized interface { 14 | Do(fn func()) 15 | DoError(fn func() *data.CodeError) *data.CodeError 16 | } 17 | 18 | func NewSynchronized(locker Locker) Synchronized { 19 | if locker == nil { 20 | locker = &sync.Mutex{} 21 | } 22 | 23 | return &synchronized{ 24 | locker: locker, 25 | } 26 | } 27 | 28 | type synchronized struct { 29 | locker Locker 30 | } 31 | 32 | func (s *synchronized) Do(fn func()) { 33 | if fn == nil { 34 | return 35 | } 36 | 37 | s.locker.Lock() 38 | fn() 39 | s.locker.Unlock() 40 | return 41 | } 42 | 43 | func (s *synchronized) DoError(fn func() *data.CodeError) (err *data.CodeError) { 44 | if fn == nil { 45 | return 46 | } 47 | 48 | s.locker.Lock() 49 | err = fn() 50 | s.locker.Unlock() 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /iqshell/common/utils/cmd.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | func IsCmdExist(cmd string) bool { 11 | _, err := exec.LookPath(cmd) 12 | return err == nil 13 | } 14 | 15 | func RunCmd(name string, params []string) (string, *data.CodeError) { 16 | c := exec.Command(name, params...) 17 | 18 | buff := &bytes.Buffer{} 19 | c.Stdout = buff 20 | c.Stdin = os.Stdin 21 | c.Stderr = os.Stderr 22 | 23 | if err := c.Run(); err != nil { 24 | return "", data.NewEmptyError().AppendDescF("cmd run error:%v", err) 25 | } 26 | return buff.String(), nil 27 | } 28 | 29 | func CmdExistBySuccess() { 30 | os.Exit(data.StatusOK) 31 | } 32 | 33 | func CmdExistByFail() { 34 | os.Exit(data.StatusError) 35 | } 36 | 37 | func CmdExistByUserCancel() { 38 | os.Exit(data.StatusUserCancel) 39 | } 40 | -------------------------------------------------------------------------------- /iqshell/common/utils/cmd_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestIsCmdExist(t *testing.T) { 6 | exist := IsCmdExist("ls") 7 | if !exist { 8 | t.Fatal("ls should exist") 9 | } 10 | 11 | exist = IsCmdExist("lss") 12 | if exist { 13 | t.Fatal("lls shouldn't exist") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /iqshell/common/utils/commits.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "bytes" 4 | 5 | var nl = []byte{'\n'} 6 | 7 | func JsonDataTrimComments(data []byte) (data1 []byte) { 8 | lines := bytes.Split(data, nl) 9 | for k, line := range lines { 10 | lines[k] = trimCommentsLine(line) 11 | } 12 | return bytes.Join(lines, nl) 13 | } 14 | 15 | func trimCommentsLine(line []byte) []byte { 16 | 17 | var newLine []byte 18 | var i, quoteCount int 19 | lastIdx := len(line) - 1 20 | for i = 0; i <= lastIdx; i++ { 21 | if line[i] == '\\' { 22 | if i != lastIdx && (line[i+1] == '\\' || line[i+1] == '"') { 23 | newLine = append(newLine, line[i], line[i+1]) 24 | i++ 25 | continue 26 | } 27 | } 28 | if line[i] == '"' { 29 | quoteCount++ 30 | } 31 | if line[i] == '#' { 32 | if quoteCount%2 == 0 { 33 | break 34 | } 35 | } 36 | if line[i] == '/' && i < lastIdx && line[i+1] == '/' { 37 | if quoteCount%2 == 0 { 38 | break 39 | } 40 | } 41 | newLine = append(newLine, line[i]) 42 | } 43 | return newLine 44 | } 45 | -------------------------------------------------------------------------------- /iqshell/common/utils/commits_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestJsonDataTrimComments01(t *testing.T) { 9 | content := ` 10 | ## commit 01 11 | aaa ## commit 02 12 | bbb 13 | ## commit 03 14 | ` 15 | contentNew := string(JsonDataTrimComments([]byte(content))) 16 | t.Log(contentNew) 17 | 18 | if strings.Contains(contentNew, "#") { 19 | t.Fatal("result content #") 20 | } 21 | 22 | if !strings.Contains(contentNew, "aaa") { 23 | t.Fatal("result has no content aaa") 24 | } 25 | 26 | if !strings.Contains(contentNew, "bbb") { 27 | t.Fatal("result has no content bbb") 28 | } 29 | } 30 | 31 | func TestJsonDataTrimComments02(t *testing.T) { 32 | content := ` 33 | // commit 01 34 | aaa // commit 02 35 | bbb 36 | // commit 03 37 | ` 38 | contentNew := string(JsonDataTrimComments([]byte(content))) 39 | t.Log(contentNew) 40 | 41 | if strings.Contains(contentNew, "//") { 42 | t.Fatal("result content //") 43 | } 44 | 45 | if !strings.Contains(contentNew, "aaa") { 46 | t.Fatal("result has no content aaa") 47 | } 48 | 49 | if !strings.Contains(contentNew, "bbb") { 50 | t.Fatal("result has no content bbb") 51 | } 52 | } 53 | 54 | func TestJsonDataTrimComments03(t *testing.T) { 55 | content := ` 56 | // commit 01 57 | "aaa" # commit 02 58 | bbb 59 | // commit 03 60 | ` 61 | contentNew := string(JsonDataTrimComments([]byte(content))) 62 | t.Log(contentNew) 63 | 64 | if strings.Contains(contentNew, "#") { 65 | t.Fatal("result content #") 66 | } 67 | 68 | if strings.Contains(contentNew, "//") { 69 | t.Fatal("result content //") 70 | } 71 | 72 | if !strings.Contains(contentNew, "aaa") { 73 | t.Fatal("result has no content aaa") 74 | } 75 | 76 | if !strings.Contains(contentNew, "bbb") { 77 | t.Fatal("result has no content bbb") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /iqshell/common/utils/empty.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func GetNotEmptyStringIfExist(values ...string) string { 4 | for _, value := range values { 5 | if len(value) > 0 { 6 | return value 7 | } 8 | } 9 | return "" 10 | } 11 | 12 | func GetTrueBoolValueIfExist(values ...bool) bool { 13 | for _, value := range values { 14 | if value { 15 | return value 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func GetNotZeroIntIfExist(values ...int) int { 22 | for _, value := range values { 23 | if value > 0 { 24 | return value 25 | } 26 | } 27 | return 0 28 | } 29 | 30 | func GetNotZeroUIntIfExist(values ...uint) uint { 31 | for _, value := range values { 32 | if value > 0 { 33 | return value 34 | } 35 | } 36 | return 0 37 | } 38 | 39 | func GetNotZeroInt64IfExist(values ...int64) int64 { 40 | for _, value := range values { 41 | if value > 0 { 42 | return value 43 | } 44 | } 45 | return 0 46 | } 47 | 48 | func GetNotZeroUInt64IfExist(values ...uint64) uint64 { 49 | for _, value := range values { 50 | if value > 0 { 51 | return value 52 | } 53 | } 54 | return 0 55 | } 56 | 57 | func GetNotZeroInt16IfExist(values ...int16) int16 { 58 | for _, value := range values { 59 | if value > 0 { 60 | return value 61 | } 62 | } 63 | return 0 64 | } 65 | 66 | func GetNotZeroUInt16IfExist(values ...uint16) uint16 { 67 | for _, value := range values { 68 | if value > 0 { 69 | return value 70 | } 71 | } 72 | return 0 73 | } 74 | 75 | func GetNotZeroInt8IfExist(values ...int8) int8 { 76 | for _, value := range values { 77 | if value > 0 { 78 | return value 79 | } 80 | } 81 | return 0 82 | } 83 | 84 | func GetNotZeroUInt8IfExist(values ...uint8) uint8 { 85 | for _, value := range values { 86 | if value > 0 { 87 | return value 88 | } 89 | } 90 | return 0 91 | } 92 | -------------------------------------------------------------------------------- /iqshell/common/utils/error.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | func IsHostUnavailableError(err error) bool { 6 | if err == nil { 7 | return false 8 | } 9 | 10 | info := err.Error() 11 | return strings.Contains(info, "dial tcp: lookup") && strings.Contains(info, ": no such host") 12 | } 13 | -------------------------------------------------------------------------------- /iqshell/common/utils/ip/group.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type groupIPParser struct { 6 | parserList []Parser 7 | } 8 | 9 | func DefaultParser() Parser { 10 | return NewGroupParser(NewAliIPParser()) 11 | } 12 | 13 | func NewGroupParser(parsers ...Parser) Parser { 14 | return &groupIPParser{ 15 | parserList: parsers, 16 | } 17 | } 18 | 19 | var _ Parser = (*groupIPParser)(nil) 20 | 21 | func (g *groupIPParser) Parse(ip string) (result ParserResult, err *data.CodeError) { 22 | if g == nil || len(g.parserList) == 0 { 23 | return nil, data.NewEmptyError().AppendDesc("no group parser") 24 | } 25 | for _, parser := range g.parserList { 26 | result, err = parser.Parse(ip) 27 | if err == nil && result != nil { 28 | break 29 | } 30 | } 31 | return 32 | } 33 | -------------------------------------------------------------------------------- /iqshell/common/utils/ip/parser.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | type ParserResult interface { 6 | } 7 | 8 | type Parser interface { 9 | Parse(ip string) (ParserResult, *data.CodeError) 10 | } 11 | -------------------------------------------------------------------------------- /iqshell/common/utils/ip_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestIsIp(t *testing.T) { 6 | ip := "10.200.20.23" 7 | if !IsIPString(ip) { 8 | t.Fatal(ip, "should be ip") 9 | } 10 | } 11 | 12 | func TestContainIPInString(t *testing.T) { 13 | ip := "10.200.20.23:80" 14 | if !IsIPUrlString(ip) { 15 | t.Fatal(ip, "should be ip") 16 | } 17 | 18 | ip = "aaa::bb:80" 19 | if !IsIPUrlString(ip) { 20 | t.Fatal(ip, "should be ip") 21 | } 22 | 23 | ip = "2001:0db8:86a3:08d3:1319:8a2e:0370:7344" 24 | if !IsIPUrlString(ip) { 25 | t.Fatal(ip, "should be ip") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /iqshell/common/utils/math.go2: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/dir_cache.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 8 | ) 9 | 10 | type DirCacheInfo struct { 11 | Dir string 12 | SaveToFile string 13 | } 14 | 15 | func (info *DirCacheInfo) Check() *data.CodeError { 16 | if len(info.Dir) == 0 { 17 | return alert.CannotEmptyError("directory path", "") 18 | } 19 | return nil 20 | } 21 | 22 | func DirCache(cfg *iqshell.Config, info DirCacheInfo) { 23 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 24 | Checker: &info, 25 | }); !shouldContinue { 26 | return 27 | } 28 | 29 | if info.SaveToFile == "" { 30 | info.SaveToFile = "stdout" 31 | } 32 | 33 | _, retErr := utils.DirCache(info.Dir, info.SaveToFile) 34 | if retErr != nil { 35 | data.SetCmdStatusError() 36 | return 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/etag.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 9 | ) 10 | 11 | type EtagInfo struct { 12 | FilePath string 13 | } 14 | 15 | func (info *EtagInfo) Check() *data.CodeError { 16 | if len(info.FilePath) == 0 { 17 | return alert.CannotEmptyError("LocalFilePath", "") 18 | } 19 | return nil 20 | } 21 | 22 | // CreateEtag 计算文件的hash值,使用七牛的etag算法 23 | func CreateEtag(cfg *iqshell.Config, info EtagInfo) { 24 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 25 | Checker: &info, 26 | }); !shouldContinue { 27 | return 28 | } 29 | 30 | if len(info.FilePath) == 0 { 31 | data.SetCmdStatusError() 32 | log.Error(alert.CannotEmpty("file path", "")) 33 | return 34 | } 35 | 36 | etag, err := utils.GetEtag(info.FilePath) 37 | if err != nil { 38 | data.SetCmdStatusError() 39 | log.Error(err) 40 | return 41 | } 42 | log.Alert(etag) 43 | } 44 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/func.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 9 | "strconv" 10 | ) 11 | 12 | type FuncCallInfo struct { 13 | FuncTemplate string 14 | ParamsJson string 15 | RunTimes string 16 | runTimesInt int 17 | } 18 | 19 | func (info *FuncCallInfo) Check() *data.CodeError { 20 | if len(info.FuncTemplate) == 0 { 21 | return alert.CannotEmptyError("FuncTemplate", "") 22 | } 23 | 24 | if len(info.ParamsJson) == 0 { 25 | return alert.CannotEmptyError("ParamsJson", "") 26 | } 27 | 28 | if len(info.RunTimes) == 0 { 29 | info.runTimesInt = 1 30 | } else { 31 | if times, err := strconv.Atoi(info.RunTimes); err != nil { 32 | return data.NewEmptyError().AppendDescF("Invalid RunTimes:%v", err) 33 | } else if times < 0 { 34 | info.runTimesInt = 0 35 | } else { 36 | info.runTimesInt = times 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func FuncCall(cfg *iqshell.Config, info FuncCallInfo) { 43 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 44 | Checker: &info, 45 | }); !shouldContinue { 46 | return 47 | } 48 | 49 | t, tErr := utils.NewTemplate(info.FuncTemplate) 50 | if tErr != nil { 51 | data.SetCmdStatusError() 52 | log.ErrorF("%v", tErr) 53 | return 54 | } 55 | 56 | if output, err := t.RunWithJsonString(info.ParamsJson); err != nil { 57 | data.SetCmdStatusError() 58 | log.ErrorF("error:%v", err) 59 | } else { 60 | log.Warning("output is insert [], and you should be careful with spaces etc.") 61 | log.InfoF("[%s]", output) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/ip.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/common/utils/ip" 9 | "time" 10 | ) 11 | 12 | type IpQueryInfo struct { 13 | Ips []string 14 | } 15 | 16 | func (info *IpQueryInfo) Check() *data.CodeError { 17 | if len(info.Ips) == 0 { 18 | return alert.CannotEmptyError("Ip", "") 19 | } 20 | return nil 21 | } 22 | 23 | func IpQuery(cfg *iqshell.Config, info IpQueryInfo) { 24 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 25 | Checker: &info, 26 | }); !shouldContinue { 27 | return 28 | } 29 | 30 | if len(info.Ips) == 0 { 31 | log.Error(data.NewEmptyError().AppendDesc(alert.CannotEmpty("ip", ""))) 32 | return 33 | } 34 | 35 | parser := ip.DefaultParser() 36 | for i, ipString := range info.Ips { 37 | if i > 0 { 38 | log.Alert("") 39 | } 40 | if result, err := parser.Parse(ipString); err != nil { 41 | log.Error(err) 42 | data.SetCmdStatusError() 43 | } else { 44 | log.AlertF("%v", result) 45 | } 46 | <-time.After(time.Millisecond * 500) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/reqid.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "github.com/qiniu/qshell/v2/iqshell" 7 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 8 | "github.com/qiniu/qshell/v2/iqshell/common/data" 9 | "github.com/qiniu/qshell/v2/iqshell/common/log" 10 | "strconv" 11 | "time" 12 | ) 13 | 14 | type ReqIdInfo struct { 15 | ReqId string 16 | } 17 | 18 | func (info *ReqIdInfo) Check() *data.CodeError { 19 | if len(info.ReqId) == 0 { 20 | return alert.CannotEmptyError("ReqId", "") 21 | } 22 | return nil 23 | } 24 | 25 | // DecodeReqId 解析reqid, 打印人工可读的字符串 26 | func DecodeReqId(cfg *iqshell.Config, info ReqIdInfo) { 27 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 28 | Checker: &info, 29 | }); !shouldContinue { 30 | return 31 | } 32 | 33 | decodedBytes, err := base64.URLEncoding.DecodeString(info.ReqId) 34 | if err != nil || len(decodedBytes) < 4 { 35 | data.SetCmdStatusError() 36 | log.Error("Invalid reqid", info.ReqId, err) 37 | return 38 | } 39 | 40 | newBytes := decodedBytes[4:] 41 | newBytesLen := len(newBytes) 42 | newStr := "" 43 | for i := newBytesLen - 1; i >= 0; i-- { 44 | newStr += fmt.Sprintf("%02X", newBytes[i]) 45 | } 46 | 47 | unixNano, err := strconv.ParseInt(newStr, 16, 64) 48 | if err != nil { 49 | data.SetCmdStatusError() 50 | log.Error("Invalid reqid", info.ReqId, err) 51 | return 52 | } 53 | 54 | dstDate := time.Unix(0, unixNano) 55 | log.AlertF("%04d-%02d-%02d/%02d-%02d", dstDate.Year(), dstDate.Month(), dstDate.Day(), 56 | dstDate.Hour(), dstDate.Minute()) 57 | } 58 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/rpc.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "bufio" 5 | "github.com/qiniu/qshell/v2/iqshell" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "os" 9 | 10 | "github.com/qiniu/qshell/v2/iqshell/common/log" 11 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 12 | ) 13 | 14 | type RpcInfo struct { 15 | Params []string 16 | } 17 | 18 | func (info *RpcInfo) Check() *data.CodeError { 19 | return nil 20 | } 21 | 22 | func RpcDecode(cfg *iqshell.Config, info RpcInfo) { 23 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 24 | Checker: &info, 25 | }); !shouldContinue { 26 | return 27 | } 28 | 29 | if len(info.Params) > 0 { 30 | for _, param := range info.Params { 31 | decodedStr, _ := utils.Decode(param) 32 | log.Alert(decodedStr) 33 | } 34 | } else { 35 | bScanner := bufio.NewScanner(os.Stdin) 36 | for bScanner.Scan() { 37 | toDecode := bScanner.Text() 38 | decodedStr, _ := utils.Decode(toDecode) 39 | log.Alert(decodedStr) 40 | } 41 | } 42 | } 43 | 44 | func RpcEncode(cfg *iqshell.Config, info RpcInfo) { 45 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 46 | Checker: &info, 47 | }); !shouldContinue { 48 | return 49 | } 50 | 51 | if len(info.Params) == 0 { 52 | data.SetCmdStatusError() 53 | log.Error(alert.CannotEmpty("Data", "")) 54 | return 55 | } 56 | 57 | for _, param := range info.Params { 58 | encodedStr := utils.Encode(param) 59 | log.Alert(encodedStr) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/url.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "net/url" 9 | ) 10 | 11 | type UrlInfo struct { 12 | Url string 13 | } 14 | 15 | func (info *UrlInfo) Check() *data.CodeError { 16 | if len(info.Url) == 0 { 17 | return alert.CannotEmptyError("Data", "") 18 | } 19 | return nil 20 | } 21 | 22 | func UrlEncode(cfg *iqshell.Config, info UrlInfo) { 23 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 24 | Checker: &info, 25 | }); !shouldContinue { 26 | return 27 | } 28 | 29 | dataEncoded := url.PathEscape(info.Url) 30 | log.Alert(dataEncoded) 31 | } 32 | 33 | func UrlDecode(cfg *iqshell.Config, info UrlInfo) { 34 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 35 | Checker: &info, 36 | }); !shouldContinue { 37 | return 38 | } 39 | 40 | dataDecoded, err := url.PathUnescape(info.Url) 41 | if err != nil { 42 | data.SetCmdStatusError() 43 | log.Error("Failed to unescape data `", info.Url, "'") 44 | } else { 45 | log.Alert(dataDecoded) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /iqshell/common/utils/operations/zip.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 9 | "os" 10 | ) 11 | 12 | type ZipInfo struct { 13 | ZipFilePath string 14 | UnzipPath string 15 | } 16 | 17 | func (info *ZipInfo) Check() *data.CodeError { 18 | if len(info.ZipFilePath) == 0 { 19 | return alert.CannotEmptyError("QiniuZipFilePath", "") 20 | } 21 | return nil 22 | } 23 | 24 | // Unzip 解压使用mkzip压缩的文件 25 | func Unzip(cfg *iqshell.Config, info ZipInfo) { 26 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 27 | Checker: &info, 28 | }); !shouldContinue { 29 | return 30 | } 31 | 32 | var err error 33 | if len(info.UnzipPath) == 0 { 34 | info.UnzipPath, err = os.Getwd() 35 | if err != nil { 36 | data.SetCmdStatusError() 37 | log.Error("Get current work directory failed due to error", err) 38 | return 39 | } 40 | } else { 41 | if _, statErr := os.Stat(info.UnzipPath); statErr != nil { 42 | data.SetCmdStatusError() 43 | log.Error("Specified is not a valid directory") 44 | return 45 | } 46 | } 47 | 48 | unzipErr := utils.Unzip(info.ZipFilePath, info.UnzipPath) 49 | if unzipErr != nil { 50 | data.SetCmdStatusError() 51 | log.Error("Unzip file failed due to error", unzipErr) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /iqshell/common/utils/os.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | ) 7 | 8 | func IsWindowsOS() bool { 9 | if runtime.GOOS == "windows" { 10 | return true 11 | } else { 12 | return false 13 | } 14 | } 15 | 16 | func IsGBKEncoding(encoding string) bool { 17 | return strings.ToLower(encoding) == "gbk" 18 | } 19 | -------------------------------------------------------------------------------- /iqshell/common/utils/path.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/mitchellh/go-homedir" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | ) 7 | 8 | func GetHomePath() (string, *data.CodeError) { 9 | if path, e := homedir.Dir(); e != nil { 10 | return "", data.NewEmptyError().AppendError(e) 11 | } else { 12 | return path, nil 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iqshell/common/utils/strings.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | // ASCII英文字母 10 | alphaList = "abcdefghijklmnopqrstuvwxyz" 11 | ) 12 | 13 | // CreateRandString 生成随机的字符串 14 | func CreateRandString(num int) (rcode string) { 15 | if num <= 0 || num > len(alphaList) { 16 | rcode = "" 17 | return 18 | } 19 | 20 | buffer := make([]byte, num) 21 | _, err := rand.Read(buffer) 22 | if err != nil { 23 | rcode = "" 24 | return 25 | } 26 | 27 | for _, b := range buffer { 28 | index := int(b) / len(alphaList) 29 | rcode += string(alphaList[index]) 30 | } 31 | 32 | return 33 | } 34 | 35 | func SplitString(line, sep string) []string { 36 | if len(sep) == 0 { 37 | //strings.TrimSpace(sep) == "" 38 | return strings.Fields(line) 39 | } 40 | return strings.Split(line, sep) 41 | } 42 | -------------------------------------------------------------------------------- /iqshell/common/utils/user_agent.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/qshell/v2/iqshell/common/version" 6 | "runtime" 7 | ) 8 | 9 | // 生成客户端代理名称 10 | func UserAgent() string { 11 | return fmt.Sprintf("QShell/%s (%s; %s; %s)", version.Version(), runtime.GOOS, runtime.GOARCH, runtime.Version()) 12 | } 13 | -------------------------------------------------------------------------------- /iqshell/common/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestBytesToReadable(t *testing.T) { 8 | sizes := map[int64]string{ 9 | 512: "512B", 10 | 1024: "1.00KB", 11 | 2048: "2.00KB", 12 | 1048576: "1.00MB", 13 | 1073741824: "1.00GB", 14 | 2073741824: "1.93GB", 15 | 1099511627776: "1.00TB", 16 | } 17 | 18 | for size, want := range sizes { 19 | got := BytesToReadable(size) 20 | if got != want { 21 | t.Fatalf("size got=%s, want=%s", got, want) 22 | } 23 | } 24 | } 25 | 26 | func TestKeyFromUrl(t *testing.T) { 27 | url := "http://vod4a6mk39q.nosdn.127.net/b258912a66334476851b698d6fe64931_1558331445602_1558331488089_2062207192-00000.mp4?download=%E7%A7%80%E7%9B%B4%E6%92%AD%E7%BC%96%E5%8F%B72114_20190520-135045_20190520-135128.mp4" 28 | want := "b258912a66334476851b698d6fe64931_1558331445602_1558331488089_2062207192-00000.mp4" 29 | 30 | key, err := KeyFromUrl(url) 31 | if err != nil { 32 | t.Fatalf("%v\n", err) 33 | } 34 | 35 | if key != want { 36 | t.Fatalf("got = %s, want = %s\n", key, want) 37 | } 38 | } 39 | 40 | func TestRemoveUrlScheme(t *testing.T) { 41 | host := "hqiniu.com" 42 | url := host 43 | result := RemoveUrlScheme(url) 44 | if host != result { 45 | t.Fatalf("RemoveUrlScheme failed, excpet:%s but:%s\n", host, result) 46 | } 47 | 48 | url = "http://" + host 49 | result = RemoveUrlScheme(url) 50 | if host != result { 51 | t.Fatalf("RemoveUrlScheme http:// failed, excpet:%s but:%s\n", host, result) 52 | } 53 | 54 | url = "https://" + host 55 | result = RemoveUrlScheme(url) 56 | if host != result { 57 | t.Fatalf("RemoveUrlScheme https:// failed, excpet:%s but:%s\n", host, result) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /iqshell/common/version/operations/version.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/common/log" 7 | "github.com/qiniu/qshell/v2/iqshell/common/version" 8 | ) 9 | 10 | type VersionInfo struct { 11 | } 12 | 13 | func (info *VersionInfo) Check() *data.CodeError { 14 | return nil 15 | } 16 | 17 | func Version(cfg *iqshell.Config, info VersionInfo) { 18 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 19 | Checker: &info, 20 | }); !shouldContinue { 21 | return 22 | } 23 | 24 | log.Alert(version.Version()) 25 | } 26 | -------------------------------------------------------------------------------- /iqshell/common/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var version = "UNSTABLE" 4 | 5 | func Version() string { 6 | return version 7 | } 8 | -------------------------------------------------------------------------------- /iqshell/common/workspace/config.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "github.com/qiniu/go-sdk/v7/auth" 5 | "github.com/qiniu/qshell/v2/iqshell/common/config" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | ) 8 | 9 | func defaultConfig() *config.Config { 10 | return &config.Config{ 11 | Credentials: &auth.Credentials{ 12 | AccessKey: "", 13 | SecretKey: nil, 14 | }, 15 | UseHttps: data.NewBool(false), 16 | Hosts: &config.Hosts{ 17 | UC: []string{"uc.qbox.me"}, 18 | }, 19 | Log: &config.LogSetting{ 20 | LogLevel: data.NewString(config.NoneKey), 21 | LogFile: nil, 22 | LogRotate: data.NewInt(7), 23 | LogStdout: data.NewBool(true), 24 | }, 25 | } 26 | } 27 | 28 | func checkConfig(cfg *config.Config) (err *data.CodeError) { 29 | // host 30 | configHostCount := 0 31 | if len(cfg.Hosts.Api) > 0 { 32 | configHostCount += 1 33 | } 34 | if len(cfg.Hosts.Rs) > 0 { 35 | configHostCount += 1 36 | } 37 | if len(cfg.Hosts.Rsf) > 0 { 38 | configHostCount += 1 39 | } 40 | if len(cfg.Hosts.Io) > 0 { 41 | configHostCount += 1 42 | } 43 | if len(cfg.Hosts.Up) > 0 { 44 | configHostCount += 1 45 | } 46 | if configHostCount != 0 && configHostCount != 5 { 47 | err = data.NewEmptyError().AppendDesc("hosts: api/rs/rsf/io/up should config all") 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /iqshell/common/workspace/interrupt.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/log" 6 | "os" 7 | "os/signal" 8 | "sync" 9 | "sync/atomic" 10 | ) 11 | 12 | var ( 13 | // 程序是否退出 14 | isCmdInterrupt uint32 = 0 15 | locker sync.Mutex 16 | cancelObservers = make([]func(s os.Signal), 0) 17 | ) 18 | 19 | func AddCancelObserver(observer func(s os.Signal)) { 20 | if observer == nil { 21 | return 22 | } 23 | 24 | locker.Lock() 25 | cancelObservers = append(cancelObservers, observer) 26 | locker.Unlock() 27 | } 28 | 29 | func notifyCancelSignalToObservers(s os.Signal) { 30 | locker.Lock() 31 | for _, observer := range cancelObservers { 32 | observer(s) 33 | } 34 | locker.Unlock() 35 | } 36 | 37 | func IsCmdInterrupt() bool { 38 | return atomic.LoadUint32(&isCmdInterrupt) > 0 39 | } 40 | 41 | func observerCmdInterrupt() { 42 | s := make(chan os.Signal, 1) 43 | signal.Notify(s, os.Interrupt, os.Kill) 44 | go func() { 45 | si := <-s 46 | log.Alert("") 47 | log.DebugF("Got signal:%s", si) 48 | atomic.StoreUint32(&isCmdInterrupt, 1) 49 | data.SetCmdStatusUserCancel() 50 | Cancel() 51 | notifyCancelSignalToObservers(si) 52 | os.Exit(data.StatusUserCancel) 53 | }() 54 | } 55 | -------------------------------------------------------------------------------- /iqshell/common/workspace/path.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | var ( 4 | // 工作路径 5 | workspaceDir = "" 6 | 7 | // 当前用户目录 8 | userDir = "" 9 | 10 | // 当前 job 所在路径 11 | jobDir = "" 12 | ) 13 | 14 | func GetWorkspace() string { 15 | return workspaceDir 16 | } 17 | 18 | func GetUserDir() string { 19 | return userDir 20 | } 21 | 22 | func GetJobDir() string { 23 | return jobDir 24 | } 25 | -------------------------------------------------------------------------------- /iqshell/storage/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/qshell/b0b048646b3a18eab15c89c5c4f453c4a2f10c30/iqshell/storage/.DS_Store -------------------------------------------------------------------------------- /iqshell/storage/bucket/bucket.go: -------------------------------------------------------------------------------- 1 | package bucket 2 | 3 | import ( 4 | "github.com/qiniu/go-sdk/v7/auth/qbox" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | 7 | "github.com/qiniu/qshell/v2/iqshell/common/client" 8 | "github.com/qiniu/qshell/v2/iqshell/common/data" 9 | "github.com/qiniu/qshell/v2/iqshell/common/workspace" 10 | ) 11 | 12 | func GetBucketManager() (manager *storage.BucketManager, err *data.CodeError) { 13 | acc, gErr := workspace.GetAccount() 14 | if gErr != nil { 15 | err = data.NewEmptyError().AppendDescF("GetBucketManager: get current account error:%v", gErr) 16 | return 17 | } 18 | 19 | mac := qbox.NewMac(acc.AccessKey, acc.SecretKey) 20 | cfg := workspace.GetStorageConfig() 21 | c := client.DefaultStorageClient() 22 | manager = storage.NewBucketManagerEx(mac, cfg, &c) 23 | return 24 | } 25 | 26 | type GetBucketApiInfo struct { 27 | Bucket string 28 | } 29 | 30 | type BucketInfo storage.BucketInfo 31 | 32 | func GetBucketInfo(info GetBucketApiInfo) (*BucketInfo, *data.CodeError) { 33 | bucketManager, err := GetBucketManager() 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if bucketInfo, gErr := bucketManager.GetBucketInfo(info.Bucket); gErr != nil { 39 | return nil, data.ConvertError(gErr) 40 | } else { 41 | return (*BucketInfo)(&bucketInfo), nil 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/create.go: -------------------------------------------------------------------------------- 1 | package bucket 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/qiniu/go-sdk/v7/auth" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 9 | "github.com/qiniu/qshell/v2/iqshell/common/workspace" 10 | ) 11 | 12 | type CreateApiInfo struct { 13 | RegionId string 14 | Bucket string 15 | Private bool 16 | } 17 | 18 | func Create(info CreateApiInfo) *data.CodeError { 19 | bucketManager, err := GetBucketManager() 20 | if err != nil { 21 | return err 22 | } 23 | 24 | cfg := workspace.GetConfig() 25 | ucHost := cfg.Hosts.GetOneUc() 26 | if len(ucHost) == 0 { 27 | return data.NewEmptyError().AppendDesc("can't get uc host") 28 | } 29 | 30 | url := utils.Endpoint(cfg.IsUseHttps(), ucHost) 31 | reqURL := fmt.Sprintf("%s/mkbucketv3/%s/region/%s/private/%v", url, info.Bucket, info.RegionId, info.Private) 32 | e := bucketManager.Client.CredentialedCall(context.Background(), bucketManager.Mac, auth.TokenQiniu, nil, "POST", reqURL, nil) 33 | return data.ConvertError(e) 34 | } 35 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/internal/list/list_v1.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "strings" 8 | ) 9 | 10 | func listBucketByV1(ctx context.Context, info ApiInfo, handler Handler) (hasMore bool, err *data.CodeError) { 11 | rets, hasMore, e := info.Manager.ListFilesWithContext(ctx, info.Bucket, 12 | storage.ListInputOptionsMarker(info.Marker), 13 | storage.ListInputOptionsPrefix(info.Prefix), 14 | storage.ListInputOptionsLimit(info.V1Limit), 15 | storage.ListInputOptionsDelimiter(info.Delimiter)) 16 | if e == nil && rets == nil { 17 | return hasMore, data.NewError(0, "v1 meet empty body when list not completed") 18 | } 19 | 20 | if e != nil { 21 | return hasMore, data.ConvertError(e) 22 | } 23 | 24 | dir := strings.Join(rets.CommonPrefixes, info.Delimiter) 25 | for _, item := range rets.Items { 26 | if handler(rets.Marker, dir, Item(item)) { 27 | break 28 | } 29 | } 30 | return hasMore, nil 31 | } 32 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/list_cache.go: -------------------------------------------------------------------------------- 1 | package bucket 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 6 | "os" 7 | ) 8 | 9 | type cacheInfo struct { 10 | Bucket string `json:"bucket"` 11 | Prefix string `json:"prefix"` 12 | Marker string `json:"marker"` 13 | } 14 | 15 | type listCache struct { 16 | enableRecord bool 17 | cachePath string 18 | } 19 | 20 | func (l *listCache) saveCache(info *cacheInfo) *data.CodeError { 21 | if !l.enableRecord || info == nil { 22 | return nil 23 | } 24 | 25 | if len(l.cachePath) == 0 { 26 | return data.NewError(0, "load cache: no cache path set, will not save record") 27 | } 28 | 29 | return utils.MarshalToFile(l.cachePath, info) 30 | } 31 | 32 | func (l *listCache) loadCache() (info *cacheInfo, err *data.CodeError) { 33 | if !l.enableRecord { 34 | return nil, nil 35 | } 36 | 37 | if len(l.cachePath) == 0 { 38 | return nil, data.NewError(0, "load cache: no cache path set, will not load record") 39 | } 40 | 41 | info = &cacheInfo{} 42 | err = utils.UnMarshalFromFile(l.cachePath, info) 43 | return 44 | } 45 | 46 | func (l *listCache) removeCache() *data.CodeError { 47 | if !l.enableRecord || len(l.cachePath) == 0 { 48 | return nil 49 | } 50 | 51 | err := os.Remove(l.cachePath) 52 | return data.ConvertError(err) 53 | } 54 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/operations/create.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/bucket" 9 | ) 10 | 11 | type CreateInfo struct { 12 | RegionId string 13 | Bucket string 14 | Private bool 15 | } 16 | 17 | func (i *CreateInfo) Check() *data.CodeError { 18 | if len(i.RegionId) == 0 { 19 | return alert.CannotEmptyError("Region", "") 20 | } 21 | if len(i.Bucket) == 0 { 22 | return alert.CannotEmptyError("Bucket", "") 23 | } 24 | return nil 25 | } 26 | 27 | func Create(cfg *iqshell.Config, info CreateInfo) { 28 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 29 | Checker: &info, 30 | }); !shouldContinue { 31 | return 32 | } 33 | 34 | if err := bucket.Create(bucket.CreateApiInfo{ 35 | RegionId: info.RegionId, 36 | Bucket: info.Bucket, 37 | Private: info.Private, 38 | }); err != nil { 39 | log.ErrorF("bucket:%s create at region:%s error:%v", info.Bucket, info.RegionId, err) 40 | } else { 41 | log.AlertF("bucket:%s create at region:%s success", info.Bucket, info.RegionId) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/operations/domain_list.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/bucket" 9 | "os" 10 | ) 11 | 12 | type ListDomainInfo struct { 13 | Bucket string 14 | Detail bool 15 | } 16 | 17 | func (info *ListDomainInfo) Check() *data.CodeError { 18 | if len(info.Bucket) == 0 { 19 | return alert.CannotEmptyError("Bucket", "") 20 | } 21 | return nil 22 | } 23 | 24 | func ListDomains(cfg *iqshell.Config, info ListDomainInfo) { 25 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 26 | Checker: &info, 27 | }); !shouldContinue { 28 | return 29 | } 30 | 31 | domains, err := bucket.AllDomainsOfBucket(info.Bucket) 32 | if err != nil { 33 | log.Error("Get domains error: ", err) 34 | os.Exit(data.StatusError) 35 | } else { 36 | if len(domains) == 0 { 37 | log.ErrorF("No domains found for bucket `%s`\n", info.Bucket) 38 | } else { 39 | if info.Detail { 40 | for _, domain := range domains { 41 | log.Alert(domain.DetailDescriptionString()) 42 | } 43 | } else { 44 | for _, domain := range domains { 45 | log.Alert(domain.DescriptionString()) 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/region.go: -------------------------------------------------------------------------------- 1 | package bucket 2 | 3 | import ( 4 | "github.com/qiniu/go-sdk/v7/storage" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/common/utils" 7 | ) 8 | 9 | func Region(b string) (*storage.Zone, *data.CodeError) { 10 | bucketManager, err := GetBucketManager() 11 | if err != nil { 12 | return nil, err 13 | } 14 | 15 | region, e := bucketManager.Zone(b) 16 | return region, data.ConvertError(e) 17 | } 18 | 19 | func CompleteBucketManagerRegion(bucketManager *storage.BucketManager, bucket string) *data.CodeError { 20 | if bucketManager == nil { 21 | return data.NewEmptyError().AppendDesc("bucketManager is empty") 22 | } 23 | 24 | if bucketManager.Cfg == nil { 25 | return data.NewEmptyError().AppendDesc("bucketManager.Cfg is empty") 26 | } 27 | 28 | if bucketManager.Cfg.Region != nil && bucketManager.Cfg.Zone != nil && len(bucketManager.Cfg.CentralRsHost) != 0 { 29 | return nil 30 | } 31 | 32 | region, e := storage.GetZone(bucketManager.Mac.AccessKey, bucket) 33 | if e != nil { 34 | return data.ConvertError(e) 35 | } 36 | bucketManager.Cfg.CentralRsHost = utils.RemoveUrlScheme(region.RsHost) 37 | bucketManager.Cfg.Region = region 38 | bucketManager.Cfg.Zone = region 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /iqshell/storage/bucket/storage_v2.go: -------------------------------------------------------------------------------- 1 | package bucket 2 | 3 | import ( 4 | "github.com/qiniu/go-sdk/v7/auth/qbox" 5 | "github.com/qiniu/go-sdk/v7/storagev2/apis" 6 | "github.com/qiniu/go-sdk/v7/storagev2/http_client" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/common/workspace" 9 | ) 10 | 11 | func GetStorageV2() (storageClient *apis.Storage, err *data.CodeError) { 12 | acc, gErr := workspace.GetAccount() 13 | if gErr != nil { 14 | err = data.NewEmptyError().AppendDescF("GetStorageV2: get current account error:%v", gErr) 15 | return 16 | } 17 | 18 | mac := qbox.NewMac(acc.AccessKey, acc.SecretKey) 19 | options := workspace.GetHttpClientOptions() 20 | options.Credentials = mac 21 | storageClient = apis.NewStorage(options) 22 | return 23 | } 24 | 25 | func GetHttpClient() (httpClient *http_client.Client, err *data.CodeError) { 26 | acc, gErr := workspace.GetAccount() 27 | if gErr != nil { 28 | err = data.NewEmptyError().AppendDescF("GetStorageV2: get current account error:%v", gErr) 29 | return 30 | } 31 | 32 | mac := qbox.NewMac(acc.AccessKey, acc.SecretKey) 33 | options := workspace.GetHttpClientOptions() 34 | options.Credentials = mac 35 | httpClient = http_client.NewClient(options) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /iqshell/storage/object/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/qshell/b0b048646b3a18eab15c89c5c4f453c4a2f10c30/iqshell/storage/object/.DS_Store -------------------------------------------------------------------------------- /iqshell/storage/object/batch/one.go: -------------------------------------------------------------------------------- 1 | package batch 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/common/data" 4 | 5 | func One(operation Operation) (*OperationResult, *data.CodeError) { 6 | results, err := Some([]Operation{operation}) 7 | if len(results) == 0 { 8 | return nil, err 9 | } 10 | return results[0], err 11 | } 12 | -------------------------------------------------------------------------------- /iqshell/storage/object/batch/some.go: -------------------------------------------------------------------------------- 1 | package batch 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/common/flow" 6 | ) 7 | 8 | func Some(operations []Operation) ([]*OperationResult, *data.CodeError) { 9 | h := &someBatchHandler{ 10 | readIndex: 0, 11 | operations: operations, 12 | results: make([]*OperationResult, 0, len(operations)), 13 | err: nil, 14 | } 15 | 16 | works := make([]flow.Work, 0, len(operations)) 17 | for _, operation := range operations { 18 | works = append(works, operation) 19 | } 20 | NewHandler(Info{ 21 | Info: flow.Info{ 22 | Force: true, 23 | WorkerCount: 1, 24 | StopWhenWorkError: true, 25 | }, 26 | WorkList: works, 27 | OperationCountPerRequest: defaultOperationCountPerRequest, 28 | }).OnResult(func(operationInfo string, operation Operation, result *OperationResult) { 29 | h.results = append(h.results, result) 30 | }).OnError(func(err *data.CodeError) { 31 | h.err = err 32 | }).Start() 33 | 34 | return h.results, h.err 35 | } 36 | 37 | type someBatchHandler struct { 38 | readIndex int 39 | operations []Operation 40 | results []*OperationResult 41 | err *data.CodeError 42 | } 43 | -------------------------------------------------------------------------------- /iqshell/storage/object/copy.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 9 | ) 10 | 11 | type CopyApiInfo struct { 12 | SourceBucket string `json:"source_bucket"` 13 | SourceKey string `json:"source_key"` 14 | DestBucket string `json:"dest_bucket"` 15 | DestKey string `json:"dest_key"` 16 | Force bool `json:"force"` 17 | } 18 | 19 | func (m *CopyApiInfo) GetBucket() string { 20 | return m.SourceBucket 21 | } 22 | 23 | func (m *CopyApiInfo) ToOperation() (string, *data.CodeError) { 24 | if len(m.SourceBucket) == 0 || len(m.SourceKey) == 0 || len(m.DestBucket) == 0 || len(m.DestKey) == 0 { 25 | return "", alert.CannotEmptyError("copy operation bucket or key of source and dest", "") 26 | } 27 | 28 | return storage.URICopy(m.SourceBucket, m.SourceKey, m.DestBucket, m.DestKey, m.Force), nil 29 | } 30 | 31 | func (m *CopyApiInfo) WorkId() string { 32 | return fmt.Sprintf("Copy|%s|%s|%s|%s", m.SourceBucket, m.SourceKey, m.DestBucket, m.DestKey) 33 | } 34 | 35 | func Copy(info *CopyApiInfo) (*batch.OperationResult, *data.CodeError) { 36 | return batch.One(info) 37 | } 38 | -------------------------------------------------------------------------------- /iqshell/storage/object/delete.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 9 | ) 10 | 11 | type DeleteApiInfo struct { 12 | Bucket string `json:"bucket"` 13 | Key string `json:"key"` 14 | DeleteAfterDays int `json:"delete_after_days"` 15 | IsDeleteAfter bool `json:"-"` 16 | Condition batch.OperationCondition `json:"condition"` 17 | } 18 | 19 | func (d *DeleteApiInfo) GetBucket() string { 20 | return d.Bucket 21 | } 22 | 23 | func (d *DeleteApiInfo) ToOperation() (string, *data.CodeError) { 24 | if len(d.Bucket) == 0 || len(d.Key) == 0 { 25 | return "", alert.CannotEmptyError("delete operation bucket or key", "") 26 | } 27 | 28 | condition := batch.OperationConditionURI(d.Condition) 29 | if d.IsDeleteAfter { 30 | if d.DeleteAfterDays < 0 { 31 | return "", alert.Error("DeleteAfterDays can't be smaller than 0", "") 32 | } else { 33 | return storage.URIDeleteAfterDays(d.Bucket, d.Key, d.DeleteAfterDays) + condition, nil 34 | } 35 | } else { 36 | return storage.URIDelete(d.Bucket, d.Key) + condition, nil 37 | } 38 | } 39 | 40 | func (d *DeleteApiInfo) WorkId() string { 41 | return fmt.Sprintf("Delete|%s|%s", d.Bucket, d.Key) 42 | } 43 | 44 | func Delete(info *DeleteApiInfo) (*batch.OperationResult, *data.CodeError) { 45 | return batch.One(info) 46 | } 47 | -------------------------------------------------------------------------------- /iqshell/storage/object/download/operations/metric.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 4 | 5 | type Metric struct { 6 | batch.Metric 7 | 8 | ExistCount int64 `json:"exist_count"` 9 | UpdateCount int64 `json:"update_count"` 10 | } 11 | 12 | func (m *Metric) AddExistCount(count int64) { 13 | if m == nil { 14 | return 15 | } 16 | m.Lock() 17 | m.ExistCount += count 18 | m.Unlock() 19 | } 20 | 21 | func (m *Metric) AddUpdateCount(count int64) { 22 | m.Lock() 23 | m.UpdateCount += count 24 | m.Unlock() 25 | } 26 | -------------------------------------------------------------------------------- /iqshell/storage/object/download/operations/utils.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/config" 5 | "github.com/qiniu/qshell/v2/iqshell/common/log" 6 | "github.com/qiniu/qshell/v2/iqshell/common/workspace" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func downloadCachePath(cfg *config.Config, downloadCfg *DownloadCfg) string { 12 | recordRoot := downloadCfg.RecordRoot 13 | if len(recordRoot) == 0 { 14 | return downloadCfg.RecordRoot 15 | } 16 | 17 | userDir := workspace.GetUserDir() 18 | if len(userDir) == 0 { 19 | log.Debug("download can't get user dir") 20 | return "" 21 | } 22 | 23 | cachePath := filepath.Join(userDir, "qdownload", downloadCfg.JobId()) 24 | if cErr := os.MkdirAll(cachePath, os.ModePerm); cErr != nil { 25 | log.WarningF("download create cache dir error:%v", cErr) 26 | return "" 27 | } 28 | return cachePath 29 | } 30 | -------------------------------------------------------------------------------- /iqshell/storage/object/exist.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "github.com/qiniu/go-sdk/v7/storage" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/bucket" 9 | ) 10 | 11 | type ExistApiInfo struct { 12 | Bucket string `json:"bucket"` 13 | Key string `json:"key"` 14 | } 15 | 16 | func Exist(info ExistApiInfo) (exists bool, err *data.CodeError) { 17 | bucketManager, err := bucket.GetBucketManager() 18 | if err != nil { 19 | return false, err 20 | } 21 | 22 | if len(info.Bucket) == 0 { 23 | return false, alert.CannotEmptyError("Bucket", "").HeaderInsertDesc("Check Exist") 24 | } 25 | if len(info.Key) == 0 { 26 | return false, alert.CannotEmptyError("Key", "").HeaderInsertDesc("Check Exist") 27 | } 28 | 29 | entry, sErr := bucketManager.Stat(info.Bucket, info.Key) 30 | if sErr != nil { 31 | if v, ok := sErr.(*storage.ErrorInfo); !ok { 32 | return false, data.NewEmptyError().AppendDescF("check file exists error, %s", sErr.Error()) 33 | } else { 34 | if v.Code != 612 { 35 | return true, nil 36 | } else { 37 | return false, data.NewEmptyError().AppendDescF("check file exists error, %s", v.Err) 38 | } 39 | } 40 | } 41 | 42 | log.DebugF("Check [%s:%s] Exist, FileHash:%s PutTime:%d", info.Bucket, info.Key, entry.Hash, entry.PutTime) 43 | if len(entry.Hash) == 0 { 44 | return false, nil 45 | } 46 | 47 | return true, nil 48 | } 49 | -------------------------------------------------------------------------------- /iqshell/storage/object/m3u8/delete.go: -------------------------------------------------------------------------------- 1 | package m3u8 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/data" 5 | "github.com/qiniu/qshell/v2/iqshell/storage/object" 6 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 7 | ) 8 | 9 | type DeleteApiInfo struct { 10 | Bucket string 11 | Key string 12 | } 13 | 14 | func Delete(info DeleteApiInfo) ([]*batch.OperationResult, *data.CodeError) { 15 | m3u8FileList, err := Slices(SliceListApiInfo{ 16 | Bucket: info.Bucket, 17 | Key: info.Key, 18 | }) 19 | 20 | if err != nil { 21 | return nil, data.NewEmptyError().AppendDesc("Get m3u8 file list error:" + err.Error()) 22 | } 23 | 24 | if len(m3u8FileList) == 0 { 25 | return nil, data.NewEmptyError().AppendDesc("no m3u8 slices found") 26 | } 27 | 28 | operations := make([]batch.Operation, 0, len(m3u8FileList)) 29 | for _, file := range m3u8FileList { 30 | operations = append(operations, &object.DeleteApiInfo{ 31 | Bucket: file.Bucket, 32 | Key: file.Key, 33 | DeleteAfterDays: 0, 34 | }) 35 | } 36 | 37 | return batch.Some(operations) 38 | } 39 | -------------------------------------------------------------------------------- /iqshell/storage/object/m3u8/operations/delete.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/m3u8" 9 | "os" 10 | ) 11 | 12 | type DeleteInfo m3u8.DeleteApiInfo 13 | 14 | func (info *DeleteInfo) Check() *data.CodeError { 15 | if len(info.Bucket) == 0 { 16 | return alert.CannotEmptyError("Bucket", "") 17 | } 18 | if len(info.Key) == 0 { 19 | return alert.CannotEmptyError("Key", "") 20 | } 21 | return nil 22 | } 23 | 24 | func Delete(cfg *iqshell.Config, info DeleteInfo) { 25 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 26 | Checker: &info, 27 | }); !shouldContinue { 28 | return 29 | } 30 | 31 | results, err := m3u8.Delete(m3u8.DeleteApiInfo(info)) 32 | for _, result := range results { 33 | if result.Code != 200 || len(result.Error) > 0 { 34 | log.ErrorF("result error:%s", result.Error) 35 | } 36 | } 37 | 38 | if err != nil { 39 | log.ErrorF("m3u8 delete error: %v", err) 40 | os.Exit(data.StatusError) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /iqshell/storage/object/m3u8/operations/replace.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/m3u8" 9 | "os" 10 | ) 11 | 12 | type ReplaceDomainInfo m3u8.ReplaceDomainApiInfo 13 | 14 | func (info *ReplaceDomainInfo) Check() *data.CodeError { 15 | if len(info.Bucket) == 0 { 16 | return alert.CannotEmptyError("Bucket", "") 17 | } 18 | if len(info.Key) == 0 { 19 | return alert.CannotEmptyError("Key", "") 20 | } 21 | return nil 22 | } 23 | 24 | func ReplaceDomain(cfg *iqshell.Config, info ReplaceDomainInfo) { 25 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 26 | Checker: &info, 27 | }); !shouldContinue { 28 | return 29 | } 30 | 31 | err := m3u8.ReplaceDomain(m3u8.ReplaceDomainApiInfo(info)) 32 | if err != nil { 33 | log.ErrorF("m3u8 replace domain error: %v", err) 34 | os.Exit(data.StatusError) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /iqshell/storage/object/mime_change.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 9 | ) 10 | 11 | type ChangeMimeApiInfo struct { 12 | Bucket string `json:"bucket"` 13 | Key string `json:"key"` 14 | Mime string `json:"mime"` 15 | } 16 | 17 | func (c *ChangeMimeApiInfo) GetBucket() string { 18 | return c.Bucket 19 | } 20 | 21 | func (c *ChangeMimeApiInfo) ToOperation() (string, *data.CodeError) { 22 | if len(c.Bucket) == 0 || len(c.Key) == 0 { 23 | return "", alert.CannotEmptyError("change mime operation bucket or key", "") 24 | } 25 | 26 | if len(c.Mime) == 0 { 27 | return "", alert.CannotEmptyError("change mime operation mime", "") 28 | } 29 | 30 | return storage.URIChangeMime(c.Bucket, c.Key, c.Mime), nil 31 | } 32 | 33 | func (c *ChangeMimeApiInfo) WorkId() string { 34 | return fmt.Sprintf("ChangeMime|%s|%s|%s", c.Bucket, c.Key, c.Mime) 35 | } 36 | 37 | func ChangeMimeType(info *ChangeMimeApiInfo) (*batch.OperationResult, *data.CodeError) { 38 | return batch.One(info) 39 | } 40 | -------------------------------------------------------------------------------- /iqshell/storage/object/move.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 9 | ) 10 | 11 | type MoveApiInfo struct { 12 | SourceBucket string `json:"source_bucket"` 13 | SourceKey string `json:"source_key"` 14 | DestBucket string `json:"dest_bucket"` 15 | DestKey string `json:"dest_key"` 16 | Force bool `json:"force"` 17 | } 18 | 19 | func (m *MoveApiInfo) GetBucket() string { 20 | return m.SourceBucket 21 | } 22 | 23 | func (m *MoveApiInfo) ToOperation() (string, *data.CodeError) { 24 | if len(m.SourceBucket) == 0 || len(m.SourceKey) == 0 || len(m.DestBucket) == 0 || len(m.DestKey) == 0 { 25 | return "", alert.CannotEmptyError("move operation bucket or key of source and dest", "") 26 | } 27 | 28 | return storage.URIMove(m.SourceBucket, m.SourceKey, m.DestBucket, m.DestKey, m.Force), nil 29 | } 30 | 31 | func (m *MoveApiInfo) WorkId() string { 32 | return fmt.Sprintf("Move|%s|%s|%s|%s", m.SourceBucket, m.SourceKey, m.DestBucket, m.DestKey) 33 | } 34 | 35 | func Move(info *MoveApiInfo) (*batch.OperationResult, *data.CodeError) { 36 | return batch.One(info) 37 | } 38 | -------------------------------------------------------------------------------- /iqshell/storage/object/operations/prefetch.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage" 9 | ) 10 | 11 | type MirrorUpdateInfo storage.PrefetchApiInfo 12 | 13 | func (info *MirrorUpdateInfo) Check() *data.CodeError { 14 | if len(info.Bucket) == 0 { 15 | return alert.CannotEmptyError("Bucket", "") 16 | } 17 | if len(info.Key) == 0 { 18 | return alert.CannotEmptyError("Key", "") 19 | } 20 | return nil 21 | } 22 | 23 | func MirrorUpdate(cfg *iqshell.Config, info MirrorUpdateInfo) { 24 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 25 | Checker: &info, 26 | }); !shouldContinue { 27 | return 28 | } 29 | 30 | err := storage.Prefetch(storage.PrefetchApiInfo(info)) 31 | if err != nil { 32 | data.SetCmdStatusError() 33 | log.ErrorF("Mirror update Failed, [%s:%s], Error: %v", info.Bucket, info.Key, err) 34 | } else { 35 | log.InfoF("Mirror update Success, [%s:%s]", info.Bucket, info.Key) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /iqshell/storage/object/operations/save_as.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 6 | "github.com/qiniu/qshell/v2/iqshell/common/data" 7 | "github.com/qiniu/qshell/v2/iqshell/common/log" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object" 9 | ) 10 | 11 | type SaveAsInfo object.SaveAsApiInfo 12 | 13 | func (info *SaveAsInfo) Check() *data.CodeError { 14 | if len(info.PublicUrl) == 0 { 15 | return alert.CannotEmptyError("PublicUrlWithFop", "") 16 | } 17 | if len(info.SaveBucket) == 0 { 18 | return alert.CannotEmptyError("SaveBucket", "") 19 | } 20 | if len(info.SaveKey) == 0 { 21 | return alert.CannotEmptyError("SaveKey", "") 22 | } 23 | return nil 24 | } 25 | 26 | func SaveAs(cfg *iqshell.Config, info SaveAsInfo) { 27 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 28 | Checker: &info, 29 | }); !shouldContinue { 30 | return 31 | } 32 | 33 | url, err := object.SaveAs(object.SaveAsApiInfo(info)) 34 | if err != nil { 35 | data.SetCmdStatusError() 36 | log.ErrorF("Save as Failed, Error: %v", err) 37 | } else { 38 | log.Alert(url) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /iqshell/storage/object/restore_archive.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 9 | ) 10 | 11 | type RestoreArchiveApiInfo struct { 12 | Bucket string `json:"bucket"` 13 | Key string `json:"key"` 14 | FreezeAfterDays int `json:"freeze_after_days"` 15 | } 16 | 17 | func (r *RestoreArchiveApiInfo) GetBucket() string { 18 | return r.Bucket 19 | } 20 | 21 | func (r *RestoreArchiveApiInfo) ToOperation() (string, *data.CodeError) { 22 | if len(r.Bucket) == 0 || len(r.Key) == 0 { 23 | return "", alert.CannotEmptyError("Restore archive operation bucket or key", "") 24 | } 25 | 26 | return storage.URIRestoreAr(r.Bucket, r.Key, r.FreezeAfterDays), nil 27 | } 28 | 29 | func (r *RestoreArchiveApiInfo) WorkId() string { 30 | return fmt.Sprintf("RestoreArchive|%s|%s|%d", r.Bucket, r.Key, r.FreezeAfterDays) 31 | } 32 | 33 | func RestoreArchive(info *RestoreArchiveApiInfo) (*batch.OperationResult, *data.CodeError) { 34 | return batch.One(info) 35 | } 36 | -------------------------------------------------------------------------------- /iqshell/storage/object/save_as.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 8 | "github.com/qiniu/qshell/v2/iqshell/common/data" 9 | "github.com/qiniu/qshell/v2/iqshell/common/workspace" 10 | "net/url" 11 | ) 12 | 13 | type SaveAsApiInfo struct { 14 | PublicUrl string `json:"public_url"` 15 | SaveBucket string `json:"save_bucket"` 16 | SaveKey string `json:"save_key"` 17 | } 18 | 19 | func SaveAs(info SaveAsApiInfo) (string, *data.CodeError) { 20 | if len(info.PublicUrl) == 0 { 21 | return "", alert.CannotEmptyError("public url", "") 22 | } 23 | 24 | uri, parseErr := url.Parse(info.PublicUrl) 25 | if parseErr != nil { 26 | return "", data.NewEmptyError().AppendDesc("parse public url error:" + parseErr.Error()) 27 | } 28 | 29 | if len(info.SaveBucket) == 0 { 30 | return "", alert.CannotEmptyError("save bucket", "") 31 | } 32 | 33 | if len(info.SaveBucket) == 0 { 34 | return "", alert.CannotEmptyError("save key", "") 35 | } 36 | 37 | acc, err := workspace.GetAccount() 38 | if err != nil { 39 | return "", err 40 | } 41 | 42 | baseUrl := uri.Host + uri.RequestURI() 43 | saveEntry := info.SaveBucket + ":" + info.SaveKey 44 | encodedSaveEntry := base64.URLEncoding.EncodeToString([]byte(saveEntry)) 45 | baseUrl += "|saveas/" + encodedSaveEntry 46 | h := hmac.New(sha1.New, []byte(acc.SecretKey)) 47 | h.Write([]byte(baseUrl)) 48 | sign := h.Sum(nil) 49 | encodedSign := base64.URLEncoding.EncodeToString(sign) 50 | return info.PublicUrl + "|saveas/" + encodedSaveEntry + "/sign/" + acc.AccessKey + ":" + encodedSign, nil 51 | } 52 | -------------------------------------------------------------------------------- /iqshell/storage/object/type_change.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | "github.com/qiniu/go-sdk/v7/storage" 6 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 9 | ) 10 | 11 | type ChangeTypeApiInfo struct { 12 | Bucket string `json:"bucket"` 13 | Key string `json:"key"` 14 | Type int `json:"type"` 15 | } 16 | 17 | func (c *ChangeTypeApiInfo) GetBucket() string { 18 | return c.Bucket 19 | } 20 | 21 | func (c *ChangeTypeApiInfo) ToOperation() (string, *data.CodeError) { 22 | if len(c.Bucket) == 0 || len(c.Key) == 0 { 23 | return "", alert.CannotEmptyError("change type operation bucket or key", "") 24 | } 25 | 26 | return storage.URIChangeType(c.Bucket, c.Key, c.Type), nil 27 | } 28 | 29 | func (c *ChangeTypeApiInfo) WorkId() string { 30 | return fmt.Sprintf("ChangeStatus|%s|%s|%d", c.Bucket, c.Key, c.Type) 31 | } 32 | 33 | func ChangeType(info *ChangeTypeApiInfo) (*batch.OperationResult, *data.CodeError) { 34 | return batch.One(info) 35 | } 36 | -------------------------------------------------------------------------------- /iqshell/storage/object/upload/api/resume_v1.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/qiniu/go-sdk/v7/storage" 7 | "github.com/qiniu/qshell/v2/iqshell/common/data" 8 | ) 9 | 10 | type resumeV1 struct { 11 | ResumeInfo 12 | 13 | uploader *storage.ResumeUploader 14 | } 15 | 16 | func (r *resumeV1) InitServer(ctx context.Context) *data.CodeError { 17 | return nil 18 | } 19 | 20 | // UploadBlock size 必须是 4M 整数倍 21 | func (r *resumeV1) UploadBlock(ctx context.Context, index int, d []byte) *data.CodeError { 22 | size := len(d) 23 | var blkCtx storage.BlkputRet 24 | err := r.uploader.Mkblk(ctx, r.TokenProvider(), r.UpHost, &blkCtx, size, bytes.NewReader(d), size) 25 | if err != nil { 26 | return data.NewEmptyError().AppendDesc("resume v1 upload block error:" + err.Error()) 27 | } else { 28 | r.Recorder.BlkCtxs = append(r.Recorder.BlkCtxs, blkCtx) 29 | r.Recorder.Offset += int64(size) 30 | return nil 31 | } 32 | } 33 | 34 | func (r *resumeV1) Complete(ctx context.Context, putRet interface{}) *data.CodeError { 35 | putExtra := storage.RputExtra{ 36 | Progresses: r.Recorder.BlkCtxs, 37 | } 38 | if err := r.uploader.Mkfile(ctx, r.TokenProvider(), r.UpHost, putRet, r.Key, true, r.Recorder.TotalSize, &putExtra); err != nil { 39 | return data.NewEmptyError().AppendDescF("resume v1 complete error:%v", err) 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /iqshell/storage/object/upload/operations/config_mould.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | const uploadConfigMouldJsonString = `{ 4 | "log_level": "debug", 5 | "log_file": "", 6 | "log_rotate": 10, 7 | "log_stdout": "true", 8 | "up_host": "", 9 | "src_dir": "", 11 | "ignore_dir": "", 12 | "skip_file_prefixes": "", 13 | "skip_path_prefixes": ".,..", 14 | "skip_fixed_strings": "", 15 | "skip_suffixes": "", 16 | "file_encoding": "", 17 | "bucket": "", 18 | "resumable_api_v2": "false", 19 | "resumable_api_v2_part_size": 1048576, 20 | "put_threshold": 16777216, 21 | "key_prefix": "", 22 | "overwrite": "true", 23 | "check_exists": "true", 24 | "check_hash": "false", 25 | "check_size": "true", 26 | "rescan_local": "false", 27 | "file_type": 0, 28 | "delete_on_success": "false", 29 | "disable_resume": "false", 30 | "disable_form": "false", 31 | "record_root": "", 32 | }` 33 | -------------------------------------------------------------------------------- /iqshell/storage/object/upload/operations/metric.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import "github.com/qiniu/qshell/v2/iqshell/storage/object/batch" 4 | 5 | type Metric struct { 6 | batch.Metric 7 | 8 | OverwriteCount int64 `json:"overwrite_count"` 9 | NotOverwriteCount int64 `json:"not_overwrite_count"` 10 | } 11 | 12 | func (m *Metric) AddOverwriteCount(count int64) { 13 | if m == nil { 14 | return 15 | } 16 | m.Lock() 17 | m.OverwriteCount += count 18 | m.Unlock() 19 | } 20 | 21 | func (m *Metric) AddNotOverwriteCount(count int64) { 22 | m.Lock() 23 | m.NotOverwriteCount += count 24 | m.Unlock() 25 | } 26 | -------------------------------------------------------------------------------- /iqshell/storage/object/upload/operations/utils.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/config" 5 | "github.com/qiniu/qshell/v2/iqshell/common/log" 6 | "github.com/qiniu/qshell/v2/iqshell/common/workspace" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | func uploadCachePath(cfg *config.Config, uploadCfg *UploadConfig) string { 12 | recordRoot := uploadCfg.RecordRoot 13 | if len(recordRoot) == 0 { 14 | return uploadCfg.RecordRoot 15 | } 16 | 17 | userDir := workspace.GetUserDir() 18 | if len(userDir) == 0 { 19 | log.Debug("upload can't get user dir") 20 | return "" 21 | } 22 | 23 | cachePath := filepath.Join(userDir, "qupload", uploadCfg.JobId()) 24 | if cErr := os.MkdirAll(cachePath, os.ModePerm); cErr != nil { 25 | log.WarningF("upload create cache dir error:%v", cErr) 26 | return "" 27 | } 28 | return cachePath 29 | } 30 | -------------------------------------------------------------------------------- /iqshell/storage/prefetch.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell/common/alert" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/storage/bucket" 7 | ) 8 | 9 | type PrefetchApiInfo struct { 10 | Bucket string 11 | Key string 12 | } 13 | 14 | func Prefetch(info PrefetchApiInfo) *data.CodeError { 15 | if len(info.Bucket) == 0 { 16 | return alert.CannotEmptyError("bucket", "") 17 | } 18 | 19 | if len(info.Key) == 0 { 20 | return alert.CannotEmptyError("key", "") 21 | } 22 | 23 | m, err := bucket.GetBucketManager() 24 | if err != nil { 25 | return err 26 | } 27 | 28 | return data.ConvertError(m.Prefetch(info.Bucket, info.Key)) 29 | } 30 | -------------------------------------------------------------------------------- /iqshell/storage/servers/operations/bucket_list.go: -------------------------------------------------------------------------------- 1 | package operations 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/iqshell" 5 | "github.com/qiniu/qshell/v2/iqshell/common/data" 6 | "github.com/qiniu/qshell/v2/iqshell/common/log" 7 | "github.com/qiniu/qshell/v2/iqshell/storage/servers" 8 | ) 9 | 10 | type ListInfo struct { 11 | servers.ListApiInfo 12 | 13 | Detail bool 14 | } 15 | 16 | func (info *ListInfo) Check() *data.CodeError { 17 | return nil 18 | } 19 | 20 | // List list 所有 bucket 21 | func List(cfg *iqshell.Config, info ListInfo) { 22 | if shouldContinue := iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{ 23 | Checker: &info, 24 | }); !shouldContinue { 25 | return 26 | } 27 | 28 | buckets, err := servers.AllBuckets(info.ListApiInfo) 29 | if err != nil { 30 | data.SetCmdStatusError() 31 | log.ErrorF("Get buckets error: %v", err) 32 | return 33 | } else if len(buckets) == 0 { 34 | log.Warning("No buckets found") 35 | return 36 | } 37 | 38 | if info.Detail { 39 | log.AlertF("%s", servers.BucketInfoDetailDescriptionStringFormat()) 40 | for _, b := range buckets { 41 | log.AlertF("%s", b.DetailDescriptionString()) 42 | } 43 | } else { 44 | for _, b := range buckets { 45 | log.AlertF("%s", b.DescriptionString()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/qiniu/qshell/v2/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /readme.go: -------------------------------------------------------------------------------- 1 | package qshell 2 | 3 | import _ "embed" 4 | 5 | //go:embed README.md 6 | var ReadMeDocument string 7 | --------------------------------------------------------------------------------