├── tsconfig.json ├── packages ├── client │ ├── .prettierrc.json │ ├── env.d.ts │ ├── auto-imports.d.ts │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── assets │ │ │ ├── logo.png │ │ │ ├── return.png │ │ │ ├── readme │ │ │ │ ├── SCR-20221217-khn.png │ │ │ │ ├── SCR-20221217-kif.png │ │ │ │ ├── SCR-20221217-kj1.png │ │ │ │ ├── SCR-20221217-kk5.png │ │ │ │ └── SCR-20221217-kl6.png │ │ │ └── main.css │ │ ├── types │ │ │ ├── image.d.ts │ │ │ └── ajax.d.ts │ │ ├── utils │ │ │ ├── format.ts │ │ │ ├── constant.ts │ │ │ └── ajax.ts │ │ ├── main.ts │ │ ├── components │ │ │ ├── QimTheme.vue │ │ │ ├── QimImageItem.vue │ │ │ ├── QimAkSkModal.vue │ │ │ ├── QimImagesList.vue │ │ │ ├── QimBucketsModal.vue │ │ │ ├── QimSunAndMoon.vue │ │ │ ├── QimUpload.vue │ │ │ └── QimImageDetail.vue │ │ ├── router │ │ │ └── index.ts │ │ ├── stores │ │ │ ├── system.ts │ │ │ ├── bucket.ts │ │ │ └── images.ts │ │ ├── views │ │ │ ├── home │ │ │ │ ├── HomeView.vue │ │ │ │ └── components │ │ │ │ │ └── ToolBar.vue │ │ │ ├── SearchView.vue │ │ │ └── BucketView.vue │ │ └── App.vue │ ├── tsconfig.config.json │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.ts │ ├── package.json │ ├── README.md │ └── components.d.ts └── server │ ├── .prettierrc │ ├── tsconfig.build.json │ ├── nest-cli.json │ ├── typing │ └── index.d.ts │ ├── test │ ├── jest-e2e.json │ └── app.e2e-spec.ts │ ├── .gitignore │ ├── src │ ├── middleware │ │ └── checkAccessKeySecretKey.ts │ ├── main.ts │ ├── app.module.ts │ ├── service │ │ └── app.service.ts │ └── controllers │ │ └── app.controller.ts │ ├── tsconfig.json │ ├── .eslintrc.js │ ├── package.json │ └── README.md ├── .npmrc ├── pnpm-workspace.yaml ├── .vscode ├── extensions.json └── settings.json ├── .editorconfig ├── .gitignore ├── Dockerfile ├── .github └── workflows │ ├── sync-gitee.yml │ ├── test.yml │ └── release.yml ├── package.json ├── scripts └── release.sh ├── LICENSE └── README.md /tsconfig.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /packages/client/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/server/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of packages/ 3 | - 'packages/*' -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/client/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /packages/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/public/favicon.ico -------------------------------------------------------------------------------- /packages/client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/logo.png -------------------------------------------------------------------------------- /packages/client/src/assets/return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/return.png -------------------------------------------------------------------------------- /packages/server/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/server/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /packages/client/src/assets/readme/SCR-20221217-khn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/readme/SCR-20221217-khn.png -------------------------------------------------------------------------------- /packages/client/src/assets/readme/SCR-20221217-kif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/readme/SCR-20221217-kif.png -------------------------------------------------------------------------------- /packages/client/src/assets/readme/SCR-20221217-kj1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/readme/SCR-20221217-kj1.png -------------------------------------------------------------------------------- /packages/client/src/assets/readme/SCR-20221217-kk5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/readme/SCR-20221217-kk5.png -------------------------------------------------------------------------------- /packages/client/src/assets/readme/SCR-20221217-kl6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JakeLaoyu/qiniu-files-manager/HEAD/packages/client/src/assets/readme/SCR-20221217-kl6.png -------------------------------------------------------------------------------- /packages/client/src/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: var(--color-text-1); 3 | background-color: var(--color-menu-light-bg); 4 | } 5 | 6 | a { 7 | text-decoration: none; 8 | color: inherit; 9 | } 10 | -------------------------------------------------------------------------------- /packages/server/typing/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'express'; 2 | 3 | declare module 'express' { 4 | interface Request { 5 | session: { 6 | accessKey: string; 7 | secretKey: string; 8 | }; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/server/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/client/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /packages/client/src/types/image.d.ts: -------------------------------------------------------------------------------- 1 | export type Image = { 2 | fsize: number; 3 | hash: string; 4 | key: string; 5 | mimeType: string; 6 | putTime: number; 7 | type: number; 8 | status: number; 9 | private: string; 10 | }; 11 | 12 | export type Folder = { 13 | key: string; 14 | mimeType: "folder"; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/client/src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export const size = (value: number) => { 2 | const kb = +Number(value / 1024).toFixed(2); 3 | if (kb < 1) return `${value} Byte`; 4 | if (kb >= 1) { 5 | const mb = +Number(kb / 1024).toFixed(2); 6 | if (mb < 1) return `${kb} Kb`; 7 | if (mb >= 1) return `${mb} Mb`; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /packages/client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-typescript", 10 | "@vue/eslint-config-prettier", 11 | ], 12 | parserOptions: { 13 | ecmaVersion: "latest", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | }, 9 | "suppressImplicitAnyIndexErrors": true 10 | }, 11 | 12 | "references": [ 13 | { 14 | "path": "./tsconfig.config.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | # .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /packages/client/src/types/ajax.d.ts: -------------------------------------------------------------------------------- 1 | import type { Image } from "./image"; 2 | 3 | export type AjaxData = { 4 | code: number; 5 | data?: T; 6 | message?: string; 7 | }; 8 | 9 | export type ImagesData = { 10 | images: Image[]; 11 | prefixs: string[]; 12 | nextMarker: string; 13 | }; 14 | 15 | export type UploadToken = { 16 | uploadToken: string; 17 | }; 18 | 19 | export type PrivateToken = { 20 | token: string; 21 | }; 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 5 | }, 6 | "[vue]": { 7 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 8 | }, 9 | "eslint.validate": [ 10 | "javascript", 11 | "javascriptreact", 12 | { "language": "typescript", "autoFix": true }, 13 | { "language": "typescriptreact", "autoFix": true } 14 | ], 15 | "typescript.tsdk": "node_modules/typescript/lib" 16 | } 17 | -------------------------------------------------------------------------------- /packages/client/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | import ArcoVueIcon from "@arco-design/web-vue/es/icon"; 4 | import "@arco-design/web-vue/es/message/style/css.js"; 5 | import "@arco-design/web-vue/es/notification/style/css.js"; 6 | 7 | import App from "./App.vue"; 8 | import router from "./router"; 9 | 10 | import "./assets/main.css"; 11 | 12 | const app = createApp(App); 13 | 14 | app.use(ArcoVueIcon); 15 | app.use(createPinia()); 16 | app.use(router); 17 | 18 | app.mount("#app"); 19 | -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM node:16.10.0 AS frontend 2 | ENV TZ=Asia/Shanghai 3 | 4 | RUN npm install pnpm@7 -g 5 | COPY . /home/qim 6 | WORKDIR /home/qim 7 | 8 | RUN pnpm install --frozen-lockfile 9 | RUN pnpm build 10 | RUN rm -rf ./**/node_modules 11 | RUN pnpm i --prod --frozen-lockfile 12 | RUN rm -rf ./packages/client/node_modules 13 | 14 | # simplify 15 | FROM --platform=$TARGETPLATFORM node:16.10.0-alpine AS backend 16 | WORKDIR /home/qim 17 | COPY --from=frontend /home/qim . 18 | WORKDIR /home/qim/packages/server 19 | 20 | ENTRYPOINT ["npm", "run", "start:prod"] -------------------------------------------------------------------------------- /packages/client/src/components/QimTheme.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /packages/server/src/middleware/checkAccessKeySecretKey.ts: -------------------------------------------------------------------------------- 1 | import { HttpCode, Injectable, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | @Injectable() 5 | export class CheckAkSkMiddleware implements NestMiddleware { 6 | @HttpCode(401) 7 | use(req: Request, res: Response, next: NextFunction) { 8 | if (!req.session.accessKey || !req.session.secretKey) { 9 | res.json({ 10 | code: 3, 11 | message: 'accessKey secretKey 不存在', 12 | }); 13 | return; 14 | } 15 | 16 | next(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/client/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | // 区域域名 2 | export const REGION: Record = { 3 | z0: "华东-浙江", 4 | "cn-east-2": "华东-浙江2", 5 | z1: "华北-河北", 6 | z2: "华南-广东", 7 | na0: "北美-洛杉矶", 8 | as0: "亚太-新加坡", 9 | "ap-northeast-1": "亚太-首尔", 10 | }; 11 | 12 | export const REGION_UPLOAD_DOMAIN = { 13 | z0: "//upload.qiniup.com", 14 | "cn-east-2": "//upload-cn-east-2.qiniup.com", 15 | z1: "//upload-z1.qiniup.com", 16 | z2: "//upload-z2.qiniup.com", 17 | na0: "//upload-na0.qiniup.com", 18 | as0: "//upload-as0.qiniup.com", 19 | "ap-northeast-1": "//upload-ap-northeast-1.qiniup.com", 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/sync-gitee.yml: -------------------------------------------------------------------------------- 1 | name: Sync to Gitee 2 | 3 | on: 4 | push: 5 | branches: [master, dev] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Sync to Gitee 12 | uses: wearerequired/git-mirror-action@master 13 | env: 14 | # 在 Settings->Secrets 配置 GITEE_RSA_PRIVATE_KEY 15 | SSH_PRIVATE_KEY: ${{ secrets.GITEE_RSA_PRIVATE_KEY }} 16 | with: 17 | # GitHub 源仓库地址 18 | source-repo: git@github.com:JakeLaoyu/qiniu-files-manager.git 19 | # Gitee 目标仓库地址 20 | destination-repo: git@gitee.com:jakelaoyu/qiniu-files-manager.git 21 | -------------------------------------------------------------------------------- /packages/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/client/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router"; 2 | import HomeView from "../views/home/HomeView.vue"; 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: "/", 9 | name: "home", 10 | component: HomeView, 11 | }, 12 | { 13 | path: "/bucket", 14 | name: "bucket", 15 | component: () => import("../views/BucketView.vue"), 16 | }, 17 | { 18 | path: "/search", 19 | name: "search", 20 | component: () => import("../views/SearchView.vue"), 21 | }, 22 | ], 23 | }); 24 | 25 | export default router; 26 | -------------------------------------------------------------------------------- /packages/server/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qiniu-files-manager", 3 | "version": "2.2.3", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev:client": "pnpm -r --parallel --filter client run dev", 8 | "dev:server": "pnpm -r --parallel --filter server run start:dev", 9 | "start:prod": "pnpm -r --parallel --filter server run start:prod", 10 | "start:pm2": "pnpm -r --parallel --filter server run start:pm2", 11 | "build": "pnpm -r --parallel --filter='./packages/*' run build", 12 | "lint": "pnpm -r --parallel --filter='./packages/*' run lint", 13 | "release": "sh ./scripts/release.sh" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "devDependencies": { 19 | "typescript": "^4.7.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | QIM 8 | 9 | 10 | 14 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/server/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import * as session from 'express-session'; 4 | import * as cookieParser from 'cookie-parser'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | 10 | if (process.env.NODE_ENV === 'development') { 11 | app.enableCors({ 12 | origin: 'http://localhost:3000', 13 | credentials: true, 14 | }); 15 | } 16 | 17 | const port = process.env.PORT || '2017'; 18 | 19 | app.use(cookieParser()); 20 | app.use( 21 | session({ 22 | cookie: { 23 | maxAge: 1000 * 60 * 60 * 24 * 30, 24 | }, 25 | resave: true, 26 | saveUninitialized: true, 27 | secret: uuidv4(), 28 | }), 29 | ); 30 | 31 | await app.listen(port); 32 | 33 | console.log(`Nest application started on http://localhost:${port}`); 34 | } 35 | 36 | bootstrap(); 37 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: TEST 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: pnpm/action-setup@v2 11 | with: 12 | version: 7 13 | run_install: false 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "14" 17 | cache: "pnpm" 18 | 19 | - name: Install dependencies 20 | run: pnpm install --frozen-lockfile 21 | 22 | - name: Lint 23 | run: pnpm lint 24 | 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: pnpm/action-setup@v2 30 | with: 31 | version: 7 32 | run_install: false 33 | - uses: actions/setup-node@v3 34 | with: 35 | node-version: "14" 36 | cache: "pnpm" 37 | 38 | - name: Install dependencies 39 | run: pnpm install --frozen-lockfile 40 | 41 | - name: Build 42 | run: pnpm build 43 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | PACKAGE_VERSION=$(node -p -e "require('./package.json').version") 4 | 5 | if [[ -z $1 ]]; then 6 | echo "Enter new version (currently $PACKAGE_VERSION): " 7 | read VERSION 8 | else 9 | VERSION=$1 10 | fi 11 | 12 | if [ $VERSION = $PACKAGE_VERSION ]; then 13 | echo "No version change, exiting" 14 | exit 1 15 | fi 16 | 17 | read -p "Releasing $VERSION - are you sure? (y/n) " -n 1 -r 18 | 19 | echo 20 | 21 | if [[ $REPLY =~ ^[Yy]$ ]]; then 22 | echo 23 | echo "* * * * * * * Releasing $VERSION * * * * * * *" 24 | echo 25 | 26 | # update package.json version to be used in the build 27 | npm version $VERSION --git-tag-version false 28 | 29 | cd ./packages/server 30 | npm version $VERSION --git-tag-version false 31 | 32 | cd ../client 33 | npm version $VERSION --git-tag-version false 34 | 35 | cd ../.. 36 | 37 | # push 38 | # git push origin refs/tags/v$VERSION 39 | git add . 40 | git commit -m "release: $VERSION" 41 | git push 42 | 43 | git checkout -b release/$VERSION 44 | git push --set-upstream origin release/$VERSION 45 | fi 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jake 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. 22 | -------------------------------------------------------------------------------- /packages/server/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { 3 | Module, 4 | NestModule, 5 | MiddlewareConsumer, 6 | RequestMethod, 7 | } from '@nestjs/common'; 8 | import { ServeStaticModule } from '@nestjs/serve-static'; 9 | import { ConfigModule } from '@nestjs/config'; 10 | 11 | import { AppController } from './controllers/app.controller'; 12 | import { AppService } from './service/app.service'; 13 | import { CheckAkSkMiddleware } from './middleware/checkAccessKeySecretKey'; 14 | 15 | @Module({ 16 | imports: [ 17 | ConfigModule.forRoot(), 18 | ServeStaticModule.forRoot({ 19 | rootPath: join(__dirname, '../..', 'client/dist'), 20 | }), 21 | ], 22 | controllers: [AppController], 23 | providers: [AppService], 24 | }) 25 | export class AppModule implements NestModule { 26 | configure(consumer: MiddlewareConsumer) { 27 | consumer.apply(CheckAkSkMiddleware).forRoutes( 28 | { 29 | path: 'api/upload-token', 30 | method: RequestMethod.GET, 31 | }, 32 | { 33 | path: 'api/private-token', 34 | method: RequestMethod.GET, 35 | }, 36 | { 37 | path: 'api/images', 38 | method: RequestMethod.GET, 39 | }, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url"; 2 | 3 | import { defineConfig } from "vite"; 4 | import vue from "@vitejs/plugin-vue"; 5 | import AutoImport from "unplugin-auto-import/vite"; 6 | import Components from "unplugin-vue-components/vite"; 7 | import { ArcoResolver } from "unplugin-vue-components/resolvers"; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | server: { 12 | port: 3000, 13 | host: "0.0.0.0", 14 | open: false, 15 | proxy: { 16 | "/api": { 17 | target: "http://localhost:2017", 18 | changeOrigin: true, // 允许跨域 19 | // rewrite: path => path.replace('/', '/'), 20 | }, 21 | }, 22 | }, 23 | plugins: [ 24 | vue(), 25 | AutoImport({ 26 | resolvers: [ArcoResolver()], 27 | }), 28 | Components({ 29 | resolvers: [ 30 | ArcoResolver({ 31 | sideEffect: true, 32 | }), 33 | ], 34 | }), 35 | ], 36 | resolve: { 37 | alias: { 38 | "@": fileURLToPath(new URL("./src", import.meta.url)), 39 | }, 40 | }, 41 | define: { 42 | // fix mime process env 43 | "process.env": {}, 44 | }, 45 | build: { 46 | rollupOptions: { 47 | output: { 48 | manualChunks: { 49 | vendor: ["vue", "vue-router", "axios", "pinia"], 50 | ui: ["@arco-design/web-vue"], 51 | }, 52 | }, 53 | }, 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "2.2.3", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit", 11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path ../../.gitignore" 12 | }, 13 | "dependencies": { 14 | "@arco-design/web-vue": "^2.40.0", 15 | "@vueuse/core": "^9.6.0", 16 | "axios": "^1.2.1", 17 | "mime": "^3.0.0", 18 | "pinia": "^2.0.28", 19 | "qs": "^6.11.0", 20 | "semver": "^7.5.2", 21 | "uuid": "^9.0.0", 22 | "vue": "^3.2.45", 23 | "vue-router": "^4.1.6" 24 | }, 25 | "devDependencies": { 26 | "@rushstack/eslint-patch": "^1.1.4", 27 | "@types/mime": "^3.0.1", 28 | "@types/mime-types": "^2.1.1", 29 | "@types/node": "^18.11.12", 30 | "@types/qs": "^6.9.7", 31 | "@types/semver": "^7.3.13", 32 | "@types/uuid": "^9.0.0", 33 | "@vitejs/plugin-vue": "^4.0.0", 34 | "@vue/eslint-config-prettier": "^7.0.0", 35 | "@vue/eslint-config-typescript": "^11.0.0", 36 | "@vue/tsconfig": "^0.1.3", 37 | "eslint": "^8.29.0", 38 | "eslint-plugin-vue": "^9.3.0", 39 | "npm-run-all": "^4.1.5", 40 | "prettier": "^2.7.1", 41 | "sass": "^1.56.2", 42 | "typescript": "^4.7.4", 43 | "unplugin-auto-import": "^0.12.1", 44 | "unplugin-vue-components": "^0.22.11", 45 | "vite": "^4.0.5", 46 | "vue-tsc": "^1.0.12" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/client/src/utils/ajax.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { Message, Notification } from "@arco-design/web-vue"; 3 | 4 | export const baseURL = import.meta.env.DEV ? "/" : location.origin; 5 | 6 | export const ajax = axios.create({ 7 | baseURL: baseURL, 8 | timeout: 30000, 9 | responseType: "json", 10 | withCredentials: import.meta.env.DEV ? true : false, 11 | }); 12 | 13 | axios.interceptors.request.use( 14 | function (config) { 15 | // Do something before request is sent 16 | // iView.LoadingBar.start(); 17 | return config; 18 | }, 19 | function (error) { 20 | // Do something with request error 21 | return Promise.reject(error); 22 | } 23 | ); 24 | 25 | ajax.interceptors.response.use( 26 | ({ data = {}, request }) => { 27 | if (request.responseURL.includes("github")) { 28 | return data; 29 | } 30 | 31 | // iView.LoadingBar.finish(); 32 | if (data && data.code !== 0) { 33 | if (data.code === 401) { 34 | Notification.error({ 35 | title: "发生错误", 36 | content: "请检查Accesskey、SecretKey是否正确", 37 | }); 38 | } else if (typeof data.message === "string") { 39 | Message.error(data.message); 40 | } else if (Array.isArray(data.message)) { 41 | data.message.forEach((item: string) => Message.error(item)); 42 | } 43 | 44 | return Promise.reject(data); 45 | } 46 | return data; 47 | }, 48 | (error: Error) => { 49 | if (axios.isCancel(error)) { 50 | console.log("Request canceled", error.message); 51 | } else { 52 | Message.error((error as Error).message || "发生错误"); 53 | } 54 | 55 | return Promise.reject(error); 56 | } 57 | ); 58 | -------------------------------------------------------------------------------- /packages/client/src/stores/system.ts: -------------------------------------------------------------------------------- 1 | import { ref } from "vue"; 2 | import { defineStore } from "pinia"; 3 | import { ajax } from "@/utils/ajax"; 4 | import type { AjaxData } from "@/types/ajax"; 5 | import { compare } from "semver"; 6 | 7 | type Status = { 8 | version: "string"; 9 | }; 10 | 11 | type GithubTag = { 12 | name: string; 13 | }; 14 | 15 | export const useSystemStore = defineStore("system", () => { 16 | const status = ref(); 17 | const githubTags = ref(); 18 | const latestVersion = ref(""); 19 | 20 | const hasNewVersion = ref(false); 21 | 22 | const getStatus = async () => { 23 | const { data } = await ajax.get>("/api/status"); 24 | status.value = data; 25 | }; 26 | 27 | const getGithubTags = async () => { 28 | const data = await ajax.get( 29 | "https://api.github.com/repos/JakeLaoyu/qiniu-files-manager/tags", 30 | { 31 | withCredentials: false, 32 | headers: { 33 | Accept: "application/vnd.github.v3.star+json", 34 | Authorization: "", 35 | }, 36 | } 37 | ); 38 | 39 | latestVersion.value = data?.[0].name.slice(1) || "0.0.0"; 40 | 41 | githubTags.value = data; 42 | }; 43 | 44 | const checkUpdate = async () => { 45 | if (!status.value) { 46 | await getStatus(); 47 | } 48 | 49 | if (!githubTags.value) { 50 | await getGithubTags(); 51 | } 52 | 53 | const { version = "" } = status.value || {}; 54 | 55 | hasNewVersion.value = compare(version, latestVersion.value) === -1; 56 | }; 57 | 58 | return { 59 | status, 60 | latestVersion, 61 | hasNewVersion, 62 | checkUpdate, 63 | }; 64 | }); 65 | -------------------------------------------------------------------------------- /packages/client/src/stores/bucket.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref } from "vue"; 2 | import { defineStore } from "pinia"; 3 | import { useStorage } from "@vueuse/core"; 4 | import { ajax } from "@/utils/ajax"; 5 | import type { AjaxData } from "@/types/ajax"; 6 | import { v4 as uuidv4 } from "uuid"; 7 | 8 | export type Bucket = { 9 | id: string; 10 | SecretKey: string; 11 | AccessKey: string; 12 | bucket: string; 13 | domains: string[]; 14 | domain: string; 15 | isPrivate: number; 16 | region: string; 17 | }; 18 | 19 | export const useBucketStore = defineStore("bucket", () => { 20 | const buckets = useStorage("buckets", []); 21 | const curBucketId = useStorage("curBucketId", ""); 22 | 23 | const showAddBucketModal = ref(false); 24 | 25 | const currentBucketInfo = computed(() => 26 | buckets.value.find((bucket) => bucket.id === curBucketId.value) 27 | ); 28 | 29 | const postSecret = async ({ accessKey = "", secretKey = "" } = {}) => { 30 | const { AccessKey, SecretKey } = currentBucketInfo.value || {}; 31 | 32 | return ajax.post("/api/secret", { 33 | accessKey: AccessKey || accessKey, 34 | secretKey: SecretKey || secretKey, 35 | }); 36 | }; 37 | 38 | const getBuckets = async () => { 39 | return ajax.get>("/api/buckets").then((data) => { 40 | if (data.data) { 41 | data.data = data.data.map((item: any) => { 42 | return { 43 | id: uuidv4(), 44 | domain: item.domains?.[0] || "", 45 | ...item, 46 | }; 47 | }); 48 | } 49 | return data; 50 | }); 51 | }; 52 | 53 | return { 54 | buckets, 55 | curBucketId, 56 | currentBucketInfo, 57 | showAddBucketModal, 58 | postSecret, 59 | getBuckets, 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /packages/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Lint with [ESLint](https://eslint.org/) 43 | 44 | ```sh 45 | npm run lint 46 | ``` 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 七牛文件管理 2 | 3 | [DEMO](http://qim.jakeyu.top) 4 | 5 | [Gitee 镜像](https://gitee.com/jakelaoyu/qiniu-files-manager) 6 | 7 | ![Alt](https://repobeats.axiom.co/api/embed/beeeb59c9f480d4ed9c99c31eabf6f555574d3db.svg "Repobeats analytics image") 8 | 9 | ## 预览 10 | 11 | ### 图片列表 12 | ![image](https://raw.githubusercontent.com/JakeLaoyu/qiniu-images-manager/master/packages/client/src/assets/readme/SCR-20221217-khn.png) 13 | 14 | ### 添加Bucket 15 | ![image](https://raw.githubusercontent.com/JakeLaoyu/qiniu-images-manager/master/packages/client/src/assets/readme/SCR-20221217-kif.png) 16 | ![image](https://raw.githubusercontent.com/JakeLaoyu/qiniu-images-manager/master/packages/client/src/assets/readme/SCR-20221217-kj1.png) 17 | 18 | ### Bucket管理 19 | ![image](https://raw.githubusercontent.com/JakeLaoyu/qiniu-images-manager/master/packages/client/src/assets/readme/SCR-20221217-kk5.png) 20 | 21 | ### 搜索 22 | ![image](https://raw.githubusercontent.com/JakeLaoyu/qiniu-images-manager/master/packages/client/src/assets/readme/SCR-20221217-kl6.png) 23 | 24 | 25 | ## 私有空间 26 | 27 | > 再添加空间时需要手动选择是否是私有空间,后面也可以在 空间管理 中进行修改。默认情况下,获取私有空间图片会401错误,因为需要获取凭证 28 | 29 | ## 部署 30 | 31 | ### docker部署 32 | 33 | ```sh 34 | docker run -d --name qim -p 2018:2017 jakeee/qim:latest 35 | ``` 36 | 37 | 部署完成后,可以在浏览器中访问 `http://127.0.0.1:2018/` 38 | 39 | ### Render 部署 40 | 41 | - [参考 memo render 部署](https://github.com/usememos/memos/blob/main/docs/deploy-with-render.md) 42 | 43 | ### 普通部署 44 | 45 | ```sh 46 | pnpm i 47 | pnpm build 48 | pnpm start:prod 49 | ``` 50 | 51 | ## 开发 52 | 53 | ```sh 54 | git clone https://github.com/JakeLaoyu/qiniu-files-manager.git 55 | cd qiniu-images-manager 56 | ``` 57 | 58 | ```sh 59 | pnpm i 60 | # 前端 61 | pnpm dev:client 62 | # 服务端 63 | pnpm dev:server 64 | ``` 65 | 66 | 访问 `http://localhost:3000/` 开始开发 67 | 68 | ## License 69 | MIT © [JakeLaoyu](https://github.com/JakeLaoyu) 70 | 71 | 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "release/*.*.*" 7 | - "pre-release/*.*.*" 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v2 16 | 17 | - name: Extract build args 18 | # Extract version from branch name 19 | # Example: branch name `release/1.0.0` sets up env.VERSION=1.0.0 20 | run: | 21 | echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v2 25 | with: 26 | install: true 27 | 28 | - name: Login to Docker Hub 29 | uses: docker/login-action@v2 30 | with: 31 | username: jakeee 32 | password: ${{ secrets.DOCKERHUB_TOKEN }} 33 | 34 | - name: Build and push 35 | uses: docker/build-push-action@v4 36 | with: 37 | # context: ./ 38 | # file: ./Dockerfile 39 | platforms: linux/amd64,linux/arm64 40 | push: true 41 | tags: jakeee/qim:latest, jakeee/qim:${{ env.VERSION }} 42 | 43 | release: 44 | needs: [docker] 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: Extract build args 49 | # Extract version from branch name 50 | # Example: branch name `release/1.0.0` sets up env.VERSION=1.0.0 51 | run: | 52 | echo "VERSION=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV 53 | 54 | - name: Bump version and push tag 55 | id: tag_version 56 | uses: mathieudutour/github-tag-action@v6.1 57 | with: 58 | github_token: ${{ secrets.GITHUB_TOKEN }} 59 | release_branches: release/* 60 | pre_release_branches: pre-release/* 61 | custom_tag: ${{ env.VERSION }} 62 | 63 | - uses: ncipollo/release-action@v1 64 | with: 65 | tag: ${{ steps.tag_version.outputs.new_tag }} 66 | name: ${{ steps.tag_version.outputs.new_tag }} 67 | body: ${{ steps.tag_version.outputs.changelog }} 68 | -------------------------------------------------------------------------------- /packages/client/src/views/home/HomeView.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 66 | 67 | 94 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "2.2.3", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "NODE_ENV=development nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "start:pm2": "pm2 start --name qim dist/main.js", 17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/common": "^9.0.0", 26 | "@nestjs/config": "^2.3.1", 27 | "@nestjs/core": "^9.0.0", 28 | "@nestjs/platform-express": "^9.0.0", 29 | "@nestjs/serve-static": "^3.0.1", 30 | "axios": "^1.2.1", 31 | "cookie-parser": "^1.4.6", 32 | "express-session": "^1.17.3", 33 | "lodash": "^4.17.21", 34 | "qiniu": "^7.8.0", 35 | "reflect-metadata": "^0.1.13", 36 | "rimraf": "^3.0.2", 37 | "rxjs": "^7.2.0", 38 | "uuid": "^9.0.0" 39 | }, 40 | "devDependencies": { 41 | "@nestjs/cli": "^9.0.0", 42 | "@nestjs/schematics": "^9.0.0", 43 | "@nestjs/testing": "^9.0.0", 44 | "@types/express": "^4.17.13", 45 | "@types/jest": "28.1.8", 46 | "@types/lodash": "^4.14.191", 47 | "@types/node": "^16.0.0", 48 | "@types/supertest": "^2.0.11", 49 | "@typescript-eslint/eslint-plugin": "^5.0.0", 50 | "@typescript-eslint/parser": "^5.0.0", 51 | "eslint": "^8.0.1", 52 | "eslint-config-prettier": "^8.3.0", 53 | "eslint-plugin-prettier": "^4.0.0", 54 | "jest": "28.1.3", 55 | "prettier": "^2.3.2", 56 | "source-map-support": "^0.5.20", 57 | "supertest": "^6.1.3", 58 | "ts-jest": "28.0.8", 59 | "ts-loader": "^9.2.3", 60 | "ts-node": "^10.0.0", 61 | "tsconfig-paths": "4.1.0", 62 | "typescript": "^4.7.4" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": ".*\\.spec\\.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "collectCoverageFrom": [ 76 | "**/*.(t|j)s" 77 | ], 78 | "coverageDirectory": "../coverage", 79 | "testEnvironment": "node" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/client/src/components/QimImageItem.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 81 | 82 | 108 | -------------------------------------------------------------------------------- /packages/client/src/components/QimAkSkModal.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 109 | 110 | 116 | -------------------------------------------------------------------------------- /packages/client/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | AAlert: typeof import('@arco-design/web-vue')['Alert'] 11 | ABackTop: typeof import('@arco-design/web-vue')['BackTop'] 12 | ABreadcrumb: typeof import('@arco-design/web-vue')['Breadcrumb'] 13 | ABreadcrumbItem: typeof import('@arco-design/web-vue')['BreadcrumbItem'] 14 | AButton: typeof import('@arco-design/web-vue')['Button'] 15 | AButtonGroup: typeof import('@arco-design/web-vue')['ButtonGroup'] 16 | ACheckbox: typeof import('@arco-design/web-vue')['Checkbox'] 17 | ACol: typeof import('@arco-design/web-vue')['Col'] 18 | ADescriptions: typeof import('@arco-design/web-vue')['Descriptions'] 19 | AForm: typeof import('@arco-design/web-vue')['Form'] 20 | AFormItem: typeof import('@arco-design/web-vue')['FormItem'] 21 | AImage: typeof import('@arco-design/web-vue')['Image'] 22 | AInput: typeof import('@arco-design/web-vue')['Input'] 23 | AInputSearch: typeof import('@arco-design/web-vue')['InputSearch'] 24 | ALink: typeof import('@arco-design/web-vue')['Link'] 25 | AList: typeof import('@arco-design/web-vue')['List'] 26 | AListItem: typeof import('@arco-design/web-vue')['ListItem'] 27 | AMenu: typeof import('@arco-design/web-vue')['Menu'] 28 | AMenuItem: typeof import('@arco-design/web-vue')['MenuItem'] 29 | AModal: typeof import('@arco-design/web-vue')['Modal'] 30 | AOption: typeof import('@arco-design/web-vue')['Option'] 31 | APopconfirm: typeof import('@arco-design/web-vue')['Popconfirm'] 32 | APopover: typeof import('@arco-design/web-vue')['Popover'] 33 | ARadio: typeof import('@arco-design/web-vue')['Radio'] 34 | ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup'] 35 | ARow: typeof import('@arco-design/web-vue')['Row'] 36 | ASelect: typeof import('@arco-design/web-vue')['Select'] 37 | ASpace: typeof import('@arco-design/web-vue')['Space'] 38 | ASpin: typeof import('@arco-design/web-vue')['Spin'] 39 | ATable: typeof import('@arco-design/web-vue')['Table'] 40 | ATag: typeof import('@arco-design/web-vue')['Tag'] 41 | ATooltip: typeof import('@arco-design/web-vue')['Tooltip'] 42 | AUpload: typeof import('@arco-design/web-vue')['Upload'] 43 | QimAkSkModal: typeof import('./src/components/QimAkSkModal.vue')['default'] 44 | QimBucketsModal: typeof import('./src/components/QimBucketsModal.vue')['default'] 45 | QimImageDetail: typeof import('./src/components/QimImageDetail.vue')['default'] 46 | QimImageItem: typeof import('./src/components/QimImageItem.vue')['default'] 47 | QimImagesList: typeof import('./src/components/QimImagesList.vue')['default'] 48 | QimSunAndMoon: typeof import('./src/components/QimSunAndMoon.vue')['default'] 49 | QimTheme: typeof import('./src/components/QimTheme.vue')['default'] 50 | QimUpload: typeof import('./src/components/QimUpload.vue')['default'] 51 | RouterLink: typeof import('vue-router')['RouterLink'] 52 | RouterView: typeof import('vue-router')['RouterView'] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/client/src/components/QimImagesList.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 130 | 131 | 137 | -------------------------------------------------------------------------------- /packages/server/README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 6 | [circleci-url]: https://circleci.com/gh/nestjs/nest 7 | 8 |

A progressive Node.js framework for building efficient and scalable server-side applications.

9 |

10 | NPM Version 11 | Package License 12 | NPM Downloads 13 | CircleCI 14 | Coverage 15 | Discord 16 | Backers on Open Collective 17 | Sponsors on Open Collective 18 | 19 | Support us 20 | 21 |

22 | 24 | 25 | ## Description 26 | 27 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 28 | 29 | ## Installation 30 | 31 | ```bash 32 | $ npm install 33 | ``` 34 | 35 | ## Running the app 36 | 37 | ```bash 38 | # development 39 | $ npm run start 40 | 41 | # watch mode 42 | $ npm run start:dev 43 | 44 | # production mode 45 | $ npm run start:prod 46 | ``` 47 | 48 | ## Test 49 | 50 | ```bash 51 | # unit tests 52 | $ npm run test 53 | 54 | # e2e tests 55 | $ npm run test:e2e 56 | 57 | # test coverage 58 | $ npm run test:cov 59 | ``` 60 | 61 | ## Support 62 | 63 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 64 | 65 | ## Stay in touch 66 | 67 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 68 | - Website - [https://nestjs.com](https://nestjs.com/) 69 | - Twitter - [@nestframework](https://twitter.com/nestframework) 70 | 71 | ## License 72 | 73 | Nest is [MIT licensed](LICENSE). 74 | -------------------------------------------------------------------------------- /packages/client/src/components/QimBucketsModal.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /packages/client/src/components/QimSunAndMoon.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | 43 | 131 | -------------------------------------------------------------------------------- /packages/client/src/App.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 113 | 114 | 179 | -------------------------------------------------------------------------------- /packages/client/src/views/SearchView.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 122 | 123 | 159 | -------------------------------------------------------------------------------- /packages/server/src/service/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as qiniu from 'qiniu'; 3 | import { Request } from 'express'; 4 | 5 | @Injectable() 6 | export class AppService { 7 | getMac(req: Request) { 8 | return new qiniu.auth.digest.Mac( 9 | req.session.accessKey, 10 | req.session.secretKey, 11 | ); 12 | } 13 | 14 | privateToken({ accessKey, secretKey, key, domain }) { 15 | const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); 16 | const config = new qiniu.conf.Config(); 17 | const bucketManager = new qiniu.rs.BucketManager(mac, config); 18 | const deadline = parseInt(`${Date.now() / 1000}`) + 3600; // 1小时过期 19 | const privateDownloadUrl = bucketManager.privateDownloadUrl( 20 | domain, 21 | key, 22 | deadline, 23 | ); 24 | return privateDownloadUrl; 25 | } 26 | 27 | uploadToken(req, bucket) { 28 | const mac = new qiniu.auth.digest.Mac( 29 | req.session.accessKey, 30 | req.session.secretKey, 31 | ); 32 | const options = { 33 | scope: bucket, 34 | callbackBody: 35 | '{"key":"$(key)","hash":"$(etag)","fsize":$(fsize),"bucket":"$(bucket)","name":"$(x:name)"}', 36 | callbackBodyType: 'application/json', 37 | }; 38 | const putPolicy = new qiniu.rs.PutPolicy(options); 39 | const uploadToken = putPolicy.uploadToken(mac); 40 | return uploadToken; 41 | } 42 | 43 | getImages({ 44 | req, 45 | bucket, 46 | prefix, 47 | costomPrefixSearch = '', 48 | search, 49 | nextMarker = '', 50 | pagesize, 51 | }): Promise { 52 | // @param options 列举操作的可选参数 53 | // prefix 列举的文件前缀 54 | // marker 上一次列举返回的位置标记,作为本次列举的起点信息 55 | // limit 每次返回的最大列举文件数量 56 | // delimiter 指定目录分隔符 57 | const options: Record = { 58 | limit: pagesize, 59 | prefix: costomPrefixSearch || prefix, 60 | }; 61 | 62 | if (!search) options.delimiter = '/'; 63 | if (nextMarker) options.marker = nextMarker; 64 | return new Promise((resolve, reject) => { 65 | this.getBucketManager(req).listPrefix( 66 | bucket, 67 | options, 68 | (err, respBody, respInfo) => { 69 | if (err) { 70 | throw err; 71 | } 72 | 73 | const nextMarker = respBody.marker; 74 | const commonPrefixes = respBody.commonPrefixes || []; 75 | const items = respBody.items; 76 | let prefixTraverseResult = {} as any; 77 | if (!search) { 78 | prefixTraverseResult = this.prefixTraverse(items, prefix); 79 | const { images } = prefixTraverseResult; 80 | 81 | resolve({ 82 | statusCode: respInfo.statusCode, 83 | respBody, 84 | images, 85 | prefixs: commonPrefixes, 86 | nextMarker, 87 | }); 88 | } else { 89 | // 返回搜索结果 90 | const findResult = items 91 | ? items.filter((item) => { 92 | return new RegExp(search).test(item.key); 93 | }) 94 | : []; 95 | resolve({ 96 | statusCode: respInfo.statusCode, 97 | respBody, 98 | images: findResult, 99 | prefixs: [], 100 | nextMarker, 101 | }); 102 | } 103 | }, 104 | ); 105 | }); 106 | } 107 | 108 | /** 109 | * 遍历前缀 110 | * @param {Array} images 七牛返回的图片数组 111 | * @return {[type]} 前缀数组 112 | */ 113 | prefixTraverse(images = [], prefix) { 114 | const prefixs = []; 115 | const imagesUrl = []; 116 | 117 | images.forEach((item) => { 118 | if (prefix) { 119 | item.key = item.key.replace(prefix, ''); 120 | } 121 | 122 | const specialPrefix = false; 123 | const itemArr = item.key.split('/'); 124 | if (itemArr.length > 1) { 125 | if (!specialPrefix && prefixs.indexOf(itemArr[0]) < 0) { 126 | prefixs.push(itemArr[0]); 127 | } else if (specialPrefix && prefixs.indexOf('/' + itemArr[0]) < 0) { 128 | prefixs.push('/' + itemArr[0]); 129 | } 130 | } else { 131 | imagesUrl.push(item); 132 | } 133 | }); 134 | 135 | const data = { 136 | prefixs: prefixs, 137 | images: imagesUrl, 138 | }; 139 | 140 | return data; 141 | } 142 | 143 | getBucketManager(req) { 144 | const mac = new qiniu.auth.digest.Mac( 145 | req.session.accessKey, 146 | req.session.secretKey, 147 | ); 148 | const config = new qiniu.conf.Config({ 149 | zone: qiniu.zone.Zone_z0, 150 | }); 151 | // config.useHttpsDomain = true; 152 | // config.zone = qiniu.zone.Zone_z0; 153 | return new qiniu.rs.BucketManager(mac, config); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /packages/client/src/components/QimUpload.vue: -------------------------------------------------------------------------------- 1 | 136 | 137 | 160 | 161 | 176 | -------------------------------------------------------------------------------- /packages/client/src/views/home/components/ToolBar.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 159 | 160 | 176 | -------------------------------------------------------------------------------- /packages/client/src/components/QimImageDetail.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 168 | 169 | 192 | -------------------------------------------------------------------------------- /packages/client/src/views/BucketView.vue: -------------------------------------------------------------------------------- 1 | 109 | 110 | 171 | 172 | 193 | -------------------------------------------------------------------------------- /packages/client/src/stores/images.ts: -------------------------------------------------------------------------------- 1 | import { ref, nextTick, computed, unref } from "vue"; 2 | import { defineStore } from "pinia"; 3 | import { useStorage } from "@vueuse/core"; 4 | import { useBucketStore } from "./bucket"; 5 | import { stringify } from "qs"; 6 | import { ajax } from "@/utils/ajax"; 7 | import axios, { type CancelTokenSource } from "axios"; 8 | import type { 9 | AjaxData, 10 | ImagesData, 11 | UploadToken, 12 | PrivateToken, 13 | } from "@/types/ajax"; 14 | import type { Image } from "@/types/image"; 15 | import { Message } from "@arco-design/web-vue"; 16 | 17 | export const useImagesStore = defineStore("images", () => { 18 | const imagesList = ref([]); 19 | const imageDetail = ref(); 20 | const prefixsOpened = useStorage("prefixsOpened", []); 21 | const prefixs = ref([]); 22 | const newPrefix = ref(""); 23 | const filterKeyword = ref(""); 24 | const nextMarker = ref(""); 25 | const multipleMode = ref(false); 26 | const selectedList = ref([]); 27 | const listHomePrefixFilter = ref(""); 28 | 29 | const hasImageListCache = ref(false); 30 | const hasPrefixsCache = ref(false); 31 | 32 | const imageListCache = ref>({}); 33 | const prefixsCache = ref>({}); 34 | 35 | const listLoading = ref(false); 36 | 37 | let cancelTokenSource: CancelTokenSource | null = null; 38 | 39 | const newPrefixFormat = computed(() => { 40 | if (!newPrefix.value) return ""; 41 | 42 | let newPrefixFormat = newPrefix.value.endsWith("/") 43 | ? newPrefix.value 44 | : newPrefix.value + "/"; 45 | 46 | newPrefixFormat = newPrefixFormat.replace(/\s/g, "-"); 47 | 48 | return newPrefixFormat; 49 | }); 50 | 51 | const resetImageStore = () => { 52 | imagesList.value = []; 53 | imageDetail.value = undefined; 54 | prefixsOpened.value = []; 55 | prefixs.value = []; 56 | newPrefix.value = ""; 57 | filterKeyword.value = ""; 58 | nextMarker.value = ""; 59 | multipleMode.value = false; 60 | }; 61 | 62 | /** 63 | * It gets a list of images from the server and stores them in the imagesList and prefixs reactive 64 | * variables 65 | * @param - query: The query parameters passed in by the user. 66 | */ 67 | const getList = async ({ query = {}, search = "" } = {}) => { 68 | const bucketStore = useBucketStore(); 69 | const { bucket, isPrivate } = bucketStore.currentBucketInfo || {}; 70 | let { domain } = bucketStore.currentBucketInfo || {}; 71 | const prefixsStr = prefixsOpened.value.join("/"); 72 | const prefix = prefixsOpened.value.length ? `${prefixsStr}/` : ""; 73 | 74 | hasPrefixsCache.value = false; 75 | hasImageListCache.value = false; 76 | 77 | if (!bucket || !domain) return; 78 | 79 | if (cancelTokenSource) { 80 | cancelTokenSource.cancel("Operation canceled by the user."); 81 | cancelTokenSource = null; 82 | } 83 | 84 | const CancelToken = axios.CancelToken; 85 | cancelTokenSource = CancelToken.source(); 86 | 87 | if (isPrivate) { 88 | domain = window.location.protocol + domain; 89 | } 90 | 91 | const queryString = stringify({ 92 | bucket, 93 | prefix: search.length ? "" : prefix, 94 | costomPrefixSearch: search || prefix ? "" : listHomePrefixFilter.value, 95 | domain, 96 | private: isPrivate, 97 | pagesize: 200, 98 | search, 99 | nextMarker: nextMarker.value, 100 | ...query, 101 | }); 102 | 103 | const preImageList = [...unref(imagesList)]; 104 | const prePrefixs = [...unref(prefixs)]; 105 | 106 | if (imageListCache.value[queryString]) { 107 | hasImageListCache.value = true; 108 | imagesList.value = [ 109 | ...imagesList.value, 110 | ...imageListCache.value[queryString], 111 | ]; 112 | } 113 | 114 | if (prefixsCache.value[queryString]) { 115 | hasPrefixsCache.value = true; 116 | prefixs.value = [...prefixs.value, ...prefixsCache.value[queryString]]; 117 | } 118 | 119 | listLoading.value = true; 120 | 121 | const { data } = 122 | (await ajax.get>(`/api/images?${queryString}`, { 123 | cancelToken: cancelTokenSource.token, 124 | })) || {}; 125 | 126 | nextTick(() => { 127 | listLoading.value = false; 128 | }); 129 | 130 | const { 131 | images, 132 | prefixs: prefixsData = [], 133 | nextMarker: nextMarkerFlag = "", 134 | } = data || {}; 135 | 136 | if (!images) return {}; 137 | 138 | if (nextMarker.value && prefixsData.length) { 139 | Message.info("有新的文件夹"); 140 | } 141 | 142 | if (!search) { 143 | images.forEach((item) => { 144 | item.key = prefix + item.key; 145 | }); 146 | 147 | imageListCache.value[queryString] = [...images]; 148 | prefixsCache.value[queryString] = [...prefixsData]; 149 | 150 | imagesList.value = [...preImageList, ...images]; 151 | prefixs.value = [...prePrefixs, ...prefixsData]; 152 | nextMarker.value = nextMarkerFlag; 153 | } 154 | 155 | return { 156 | nextMarker: nextMarkerFlag, 157 | images, 158 | }; 159 | }; 160 | 161 | const getImageDetail = async (image: Image) => { 162 | const bucketStore = useBucketStore(); 163 | const { bucket } = bucketStore.currentBucketInfo || {}; 164 | 165 | const res = await ajax.get>("/api/detail", { 166 | params: { 167 | key: image.key, 168 | bucket: bucket, 169 | }, 170 | }); 171 | 172 | imageDetail.value = { 173 | ...image, 174 | ...res.data, 175 | }; 176 | }; 177 | 178 | const deleteImage = async (image: Image | string[]) => { 179 | const bucketStore = useBucketStore(); 180 | const { bucket } = bucketStore.currentBucketInfo || {}; 181 | 182 | return ajax 183 | .delete>("/api/image", { 184 | data: { 185 | key: Array.isArray(image) ? image : image.key, 186 | bucket: bucket, 187 | }, 188 | }) 189 | .then((res) => { 190 | if (res.code === 0) { 191 | imagesList.value = imagesList.value.filter((item) => { 192 | if (Array.isArray(image)) { 193 | return !image.includes(item.key); 194 | } 195 | return item.key !== image.key; 196 | }); 197 | } 198 | return res; 199 | }); 200 | }; 201 | 202 | const getUploadToken = async () => { 203 | const bucketStore = useBucketStore(); 204 | const { bucket } = bucketStore.currentBucketInfo || {}; 205 | 206 | if (!bucket) return; 207 | 208 | await bucketStore.postSecret(); 209 | 210 | // 获取token 211 | const { data } = await ajax.get>( 212 | `/api/upload-token?bucket=${bucket}` 213 | ); 214 | 215 | const { uploadToken } = data || {}; 216 | 217 | return uploadToken; 218 | }; 219 | 220 | const getPrivateToken = async (key: string) => { 221 | const bucketStore = useBucketStore(); 222 | const { domain } = bucketStore.currentBucketInfo || {}; 223 | 224 | // 获取token 225 | const { data } = await ajax.get>( 226 | `/api/private-token`, 227 | { 228 | params: { 229 | key, 230 | domain: window.location.protocol + domain, 231 | }, 232 | } 233 | ); 234 | 235 | const { token } = data || {}; 236 | 237 | return token; 238 | }; 239 | 240 | const moveImage = async (image: Image, newKey: string) => { 241 | const bucketStore = useBucketStore(); 242 | const { bucket } = bucketStore.currentBucketInfo || {}; 243 | 244 | return ajax.post>("/api/move-image", { 245 | bucket: bucket, 246 | key: image.key, 247 | newKey: newKey, 248 | }); 249 | }; 250 | 251 | const getImageUrl = (image?: Image) => { 252 | if (!image) return ""; 253 | 254 | const bucketStore = useBucketStore(); 255 | const { domain, isPrivate } = bucketStore.currentBucketInfo || {}; 256 | 257 | if (isPrivate) { 258 | return `${domain}${image.key}?${image.private}`; 259 | } 260 | 261 | return `//${domain}/${image.key}`; 262 | }; 263 | 264 | const multipleMoveImage = (newKey: string) => { 265 | const bucketStore = useBucketStore(); 266 | const { bucket } = bucketStore.currentBucketInfo || {}; 267 | 268 | return ajax.post>("/api/multiple-move-image", { 269 | bucket: bucket, 270 | keys: selectedList.value, 271 | newKey: newKey, 272 | }); 273 | }; 274 | 275 | return { 276 | listLoading, 277 | imagesList, 278 | prefixsOpened, 279 | prefixs, 280 | newPrefix, 281 | newPrefixFormat, 282 | imageDetail, 283 | filterKeyword, 284 | nextMarker, 285 | multipleMode, 286 | selectedList, 287 | listHomePrefixFilter, 288 | hasPrefixsCache, 289 | hasImageListCache, 290 | resetImageStore, 291 | getList, 292 | getUploadToken, 293 | getImageDetail, 294 | deleteImage, 295 | getImageUrl, 296 | getPrivateToken, 297 | moveImage, 298 | multipleMoveImage, 299 | }; 300 | }); 301 | -------------------------------------------------------------------------------- /packages/server/src/controllers/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Req, Res, Delete } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import * as qiniu from 'qiniu'; 4 | import * as lodash from 'lodash'; 5 | import axios from 'axios'; 6 | import { AppService } from '../service/app.service'; 7 | 8 | const QiniuApi = { 9 | buckets: 'https://rs.qbox.me/buckets', 10 | domainList: 'https://api.qiniu.com/v6/domain/list?tbl=', 11 | }; 12 | 13 | @Controller('/api') 14 | export class AppController { 15 | constructor(private readonly appService: AppService) {} 16 | 17 | @Get('private-token') 18 | getPrivateToken(@Req() req: Request) { 19 | if (!req.query.domain) { 20 | return { 21 | code: 1, 22 | message: 'domain is required', 23 | }; 24 | } 25 | 26 | const domain = req.query.domain as string; 27 | 28 | const token = this.appService.privateToken({ 29 | accessKey: req.session.accessKey, 30 | secretKey: req.session.secretKey, 31 | key: req.query.key, 32 | domain: domain.substring(0, domain.length - 1), 33 | }); 34 | 35 | return { 36 | code: 0, 37 | data: { 38 | token: token.split('?')[1], 39 | }, 40 | }; 41 | } 42 | 43 | // get buckets by ak & sk 44 | @Get('buckets') 45 | async getBuckets(@Req() req: Request) { 46 | let result: { 47 | data?: any[]; 48 | } = {}; 49 | 50 | try { 51 | result = await axios({ 52 | url: QiniuApi.buckets, 53 | method: 'get', 54 | headers: { 55 | Authorization: qiniu.util.generateAccessToken( 56 | this.appService.getMac(req), 57 | QiniuApi.buckets, 58 | null, 59 | ), 60 | }, 61 | }); 62 | } catch (error) { 63 | console.error(error); 64 | return { 65 | code: error.response.status, 66 | message: '请填写合法AccessKey、SecretKey', 67 | }; 68 | } 69 | 70 | if (result.data && result.data.length) { 71 | const getDomainFunList = []; 72 | 73 | result.data.forEach((item) => { 74 | getDomainFunList.push( 75 | axios({ 76 | url: QiniuApi.domainList + item, 77 | method: 'get', 78 | headers: { 79 | Authorization: qiniu.util.generateAccessToken( 80 | this.appService.getMac(req), 81 | QiniuApi.domainList + item, 82 | null, 83 | ), 84 | }, 85 | }).then((domain) => { 86 | return { 87 | AccessKey: req.session.accessKey, 88 | SecretKey: req.session.secretKey, 89 | bucket: item, 90 | domains: domain.data, 91 | isPrivate: 0, 92 | // 空间区域,默认华东 93 | region: 'z0', 94 | }; 95 | }), 96 | ); 97 | }); 98 | 99 | const domainList = await Promise.all(getDomainFunList); 100 | 101 | return { 102 | code: 0, 103 | data: domainList, 104 | }; 105 | } else { 106 | return { 107 | code: 1, 108 | message: '发生错误,请检查AccessKey、SecretKey是否填写正确', 109 | }; 110 | } 111 | } 112 | 113 | // save ak sk 114 | @Post('secret') 115 | postSecret(@Req() req: Request) { 116 | console.log('req.body.accessKey', req.body.accessKey); 117 | console.log('req.body.secretKey', req.body.secretKey); 118 | req.session.accessKey = req.body.accessKey; 119 | req.session.secretKey = req.body.secretKey; 120 | 121 | return { 122 | code: 0, 123 | }; 124 | } 125 | 126 | @Get('images') 127 | async getImages(@Req() req: Request) { 128 | const bucket = req.query.bucket; 129 | const domain = req.query.domain as string; 130 | const isPrivate = req.query.private; 131 | const search = req.query.search || ''; 132 | const prefix = req.query.prefix || ''; 133 | const costomPrefixSearch = (req.query.costomPrefixSearch as string) || ''; 134 | const nextMarker = req.query.nextMarker as string; 135 | const pagesize = req.query.pagesize || 50; 136 | 137 | const result = await this.appService.getImages({ 138 | req, 139 | bucket, 140 | prefix, 141 | costomPrefixSearch, 142 | search, 143 | nextMarker, 144 | pagesize, 145 | }); 146 | if (isPrivate) { 147 | // 私有空间获取凭证 148 | result.images && 149 | result.images.forEach((item) => { 150 | item.private = this.appService 151 | .privateToken({ 152 | accessKey: req.session.accessKey, 153 | secretKey: req.session.secretKey, 154 | key: `${prefix}${item.key}`, 155 | domain: domain.substring(0, domain.length - 1), 156 | }) 157 | .split('?')[1]; 158 | }); 159 | } 160 | 161 | result.prefixs = result.prefixs.map((item) => { 162 | let str = item; 163 | if (prefix && !costomPrefixSearch) str = item.replace(prefix, ''); 164 | str = str.slice(0, str.length - 1); 165 | return str; 166 | }); 167 | 168 | return { 169 | code: result.statusCode === 200 ? 0 : result.statusCode, 170 | data: { 171 | images: result.images || [], 172 | prefixs: result.prefixs || [], 173 | nextMarker: result.nextMarker, 174 | }, 175 | message: 176 | result.statusCode === 200 177 | ? '' 178 | : lodash.get(result, 'resultpBody.error', '发生错误'), 179 | }; 180 | } 181 | 182 | // 获取token 183 | @Get('upload-token') 184 | uploadToken(@Req() req: Request) { 185 | const Bucket = req.query.bucket; 186 | 187 | const token = this.appService.uploadToken(req, Bucket); 188 | 189 | return { 190 | code: 0, 191 | data: { 192 | uploadToken: token, 193 | }, 194 | }; 195 | } 196 | 197 | @Delete('session') 198 | delSession(@Req() req: Request) { 199 | req.session.accessKey = ''; 200 | req.session.secretKey = ''; 201 | 202 | return { 203 | code: 0, 204 | }; 205 | } 206 | 207 | @Get('detail') 208 | detail(@Req() req: Request, @Res() res: Response) { 209 | const key = req.query.key as string; 210 | const bucket = req.query.bucket as string; 211 | 212 | this.appService 213 | .getBucketManager(req) 214 | .stat(bucket, key, function (err, respBody, respInfo) { 215 | if (err) { 216 | console.log(err); 217 | } else { 218 | if (respInfo.statusCode === 200) { 219 | res.json({ 220 | code: 0, 221 | data: respBody, 222 | }); 223 | } else { 224 | console.log(respInfo.statusCode); 225 | console.log(respBody.error); 226 | res.json({ 227 | code: 1, 228 | message: respBody.error, 229 | }); 230 | } 231 | } 232 | }); 233 | } 234 | 235 | @Delete('image') 236 | delImage(@Req() req: Request, @Res() res: Response) { 237 | const key = req.body.key; 238 | const bucket = req.body.bucket; 239 | const deleteOperations = []; 240 | 241 | if (key instanceof Array) { 242 | if (key.length > 1000) { 243 | return { 244 | code: 0, 245 | message: '单次最多1000个文件', 246 | }; 247 | } 248 | key.forEach((item) => { 249 | deleteOperations.push(qiniu.rs.deleteOp(bucket, item)); 250 | }); 251 | this.appService 252 | .getBucketManager(req) 253 | .batch(deleteOperations, function (err, respBody, respInfo) { 254 | if (err) { 255 | console.log(err); 256 | // throw err; 257 | } else { 258 | console.log(respInfo.statusCode); 259 | console.log(respBody); 260 | res.json({ 261 | code: 0, 262 | info: respBody, 263 | }); 264 | } 265 | }); 266 | } else { 267 | this.appService 268 | .getBucketManager(req) 269 | .delete(bucket, key, function (err, respBody, respInfo) { 270 | if (err) { 271 | console.log(err); 272 | // throw err; 273 | } else { 274 | console.log(respInfo.statusCode); 275 | console.log(respBody); 276 | 277 | res.json({ 278 | code: 0, 279 | info: respBody, 280 | }); 281 | } 282 | }); 283 | } 284 | } 285 | 286 | @Post('move-image') 287 | moveImage(@Req() req: Request, @Res() res: Response) { 288 | const key = req.body.key; 289 | const newKey = req.body.newKey; 290 | const bucket = req.body.bucket; 291 | 292 | // 强制覆盖已有同名文件 293 | const options = { 294 | force: false, 295 | }; 296 | 297 | this.appService 298 | .getBucketManager(req) 299 | .move( 300 | bucket, 301 | key, 302 | bucket, 303 | newKey, 304 | options, 305 | function (err, respBody, respInfo) { 306 | if (err) { 307 | console.log(err); 308 | // throw err; 309 | } else { 310 | // 200 is success 311 | console.log(respBody); 312 | if (respInfo.statusCode === 614) { 313 | return res.json({ 314 | code: 1, 315 | message: '文件名重复', 316 | }); 317 | } 318 | res.json({ 319 | code: 0, 320 | }); 321 | } 322 | }, 323 | ); 324 | } 325 | 326 | @Post('multiple-move-image') 327 | multipleMoveImage(@Req() req: Request, @Res() res: Response) { 328 | const keys = req.body.keys; 329 | const bucket = req.body.bucket; 330 | let newKey = req.body.newKey; 331 | 332 | newKey = newKey.substr(1); 333 | 334 | const moveOperations = []; 335 | 336 | if (keys.length <= 1000) { 337 | keys.forEach((item) => { 338 | moveOperations.push( 339 | qiniu.rs.moveOp( 340 | bucket, 341 | item, 342 | bucket, 343 | `${newKey}${item.split('/').pop()}`, 344 | ), 345 | ); 346 | }); 347 | } else { 348 | return res.json({ 349 | code: 1, 350 | message: '单次最多1000个文件', 351 | }); 352 | } 353 | 354 | this.appService 355 | .getBucketManager(req) 356 | .batch(moveOperations, function (err, respBody, respInfo) { 357 | if (err) { 358 | console.log(err); 359 | // throw err; 360 | } else { 361 | const errList = []; 362 | // 200 is success, 298 is part success 363 | if (parseInt(`${respInfo.statusCode / 100}`) === 2) { 364 | respBody.forEach(function (item) { 365 | if (item.code === 200) { 366 | // console.log(item.code + '\tsuccess') 367 | } else { 368 | errList.push(item.code + '\t' + item.data.error); 369 | } 370 | }); 371 | 372 | if (errList.length === 0) { 373 | res.json({ 374 | code: 0, 375 | }); 376 | } else { 377 | res.json({ 378 | code: 1, 379 | message: errList, 380 | }); 381 | } 382 | } else { 383 | res.json({ 384 | code: 0, 385 | info: respBody, 386 | }); 387 | } 388 | } 389 | }); 390 | } 391 | 392 | @Get('status') 393 | getStatus(@Req() req: Request, @Res() res: Response) { 394 | res.json({ 395 | code: 0, 396 | data: { 397 | version: process.env.npm_package_version, 398 | }, 399 | }); 400 | } 401 | } 402 | --------------------------------------------------------------------------------