├── .dockerignore ├── images ├── login.png └── upload.png ├── .prettierrc ├── worker-configuration.d.ts ├── .editorconfig ├── test ├── tsconfig.json └── index.spec.ts ├── vitest.config.ts ├── Dockerfile ├── entrypoint.sh ├── wrangler.toml ├── .github └── workflows │ ├── deploy.yml │ └── docker-image.yml ├── package.json ├── .gitignore ├── README.md ├── src ├── index.ts └── index.html └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | node_modules/ 3 | images/ 4 | README.md 5 | test/ 6 | -------------------------------------------------------------------------------- /images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiaofei123/image-hosting-cfworkers/HEAD/images/login.png -------------------------------------------------------------------------------- /images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lixiaofei123/image-hosting-cfworkers/HEAD/images/upload.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "semi": true, 5 | "useTabs": true 6 | } 7 | -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler 2 | // After adding bindings to `wrangler.toml`, regenerate this interface via `npm run cf-typegen` 3 | interface Env { 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.yml] 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@cloudflare/workers-types/experimental", 6 | "@cloudflare/vitest-pool-workers" 7 | ] 8 | }, 9 | "include": ["./**/*.ts", "../src/env.d.ts"], 10 | "exclude": [] 11 | } 12 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; 2 | 3 | export default defineWorkersConfig({ 4 | test: { 5 | poolOptions: { 6 | workers: { 7 | wrangler: { configPath: "./wrangler.toml" }, 8 | }, 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-bullseye-slim 2 | 3 | # Install dependencies 4 | RUN apt-get -qq update && \ 5 | apt-get install -qqy --no-install-recommends \ 6 | ca-certificates \ 7 | libc++-dev && \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | WORKDIR /usr/src/app 11 | 12 | COPY package*.json ./ 13 | 14 | RUN npm install 15 | 16 | COPY . . 17 | 18 | EXPOSE 8080 19 | 20 | CMD ["sh","entrypoint.sh"] 21 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUCKET_NAME=$BUCKET_NAME 4 | PASSWORD=$PASSWORD 5 | SIGN_KEY=$SIGN_KEY 6 | 7 | 8 | if [ -n "$PASSWORD" ]; then 9 | sed -i "s/PASSWORD = .*/PASSWORD = \"$PASSWORD\"/" wrangler.toml 10 | fi 11 | 12 | if [ -n "$SIGN_KEY" ]; then 13 | sed -i "s/SIGN_KEY = .*/SIGN_KEY = \"$SIGN_KEY\"/" wrangler.toml 14 | fi 15 | 16 | node_modules/wrangler/bin/wrangler.js dev --port 8080 --ip 0.0.0.0 17 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "image-hosting-cfworkers" 3 | main = "src/index.ts" 4 | compatibility_date = "2024-05-02" 5 | compatibility_flags = ["nodejs_compat"] 6 | 7 | 8 | 9 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 10 | # Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#r2-buckets 11 | 12 | [[r2_buckets]] 13 | binding = "MY_BUCKET" 14 | bucket_name = "myimages" 15 | 16 | 17 | [vars] 18 | R2_DOMAIN = "" 19 | PASSWORD = "" 20 | SIGN_KEY = "" 21 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Worker 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | name: Deploy 11 | steps: 12 | - uses: cloudflare/wrangler-action@v3 13 | - name: Deploy 14 | uses: cloudflare/wrangler-action@v3 15 | with: 16 | apiToken: ${{ secrets.CF_API_TOKEN }} 17 | accountId: ${{ secrets.CF_ACCOUNT_ID }} 18 | env: 19 | R2_DOMAIN: ${{ secrets.R2_DOMAIN }} 20 | PASSWORD: ${{ secrets.PASSWORD }} 21 | SIGN_KEY: ${{ secrets.SIGN_KEY }} 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-hosting-cfworkers", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "test": "vitest", 10 | "cf-typegen": "wrangler types" 11 | }, 12 | "devDependencies": { 13 | "@cloudflare/vitest-pool-workers": "^0.1.0", 14 | "@cloudflare/workers-types": "^4.20240502.0", 15 | "typescript": "^5.0.4", 16 | "vitest": "1.3.0", 17 | "wrangler": "^3.0.0" 18 | }, 19 | "dependencies": { 20 | "@tsndr/cloudflare-worker-jwt": "^2.5.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v2 15 | - 16 | name: Set up QEMU 17 | uses: docker/setup-qemu-action@v1 18 | - 19 | name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v1 21 | - 22 | name: Login to DockerHub 23 | uses: docker/login-action@v1 24 | with: 25 | username: ${{ secrets.DOCKERHUB_USERNAME }} 26 | password: ${{ secrets.DOCKERHUB_TOKEN }} 27 | - 28 | name: Build and push 29 | uses: docker/build-push-action@v2 30 | with: 31 | context: . 32 | platforms: | 33 | linux/amd64 34 | linux/arm64 35 | push: true 36 | tags: mrlee326/imagehosting -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- 1 | // test/index.spec.ts 2 | import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; 3 | import { describe, it, expect } from 'vitest'; 4 | import worker from '../src/index'; 5 | 6 | // For now, you'll need to do something like this to get a correctly-typed 7 | // `Request` to pass to `worker.fetch()`. 8 | const IncomingRequest = Request; 9 | 10 | describe('Hello World worker', () => { 11 | it('responds with Hello World! (unit style)', async () => { 12 | const request = new IncomingRequest('http://example.com'); 13 | // Create an empty context to pass to `worker.fetch()`. 14 | const ctx = createExecutionContext(); 15 | const response = await worker.fetch(request, env, ctx); 16 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions 17 | await waitOnExecutionContext(ctx); 18 | expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); 19 | }); 20 | 21 | it('responds with Hello World! (integration style)', async () => { 22 | const response = await SELF.fetch('https://example.com'); 23 | expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | 174 | .vscode/ 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 安装说明 2 | 3 | 基于Cloudflare R2对象存储和Workers的免费图床程序,安装前请确认自己已经拥有Cloudflare账号和已经开通Cloudflare R2对象存储功能。 4 | 5 | ### 基于Clloudflare Workers间接访问R2存储,不怕被刷, 6 | 7 | 功能特性 8 | - 支持密码 9 | - 支持拖拽、文件选取、直接粘贴文件、直接粘贴URL 四种上传方式 10 | - 支持批量上传 11 | - 完全基于Cloudflare,零成本搭建(可能需要一个自己的域名) 12 | 13 | 下面这个特性,如果你不知道就太可惜了。 14 | ## 你可以直接用QQ等截图工具截图后,无需保存,在页面上直接CRTL+V粘贴即可自动上传。也可以在别的网页上看到喜欢的图片后,右键复制图片或者复制图片地址,然后在图床页面CRTL+V粘贴上传。 15 | 16 | 17 | 18 | 21 | 24 | 25 |
19 | 20 | 22 | 23 |
26 | 27 | [不推荐使用此方法] 本图床也提供了Docker镜像运行的方式,此方式不依赖Cloudflare以及R2对象, 命令如下 28 | 29 | > 如果使用Docker运行本程序的话,需要使用Nginx之类的服务用HTTPS协议来代理本服务,否则页面上的复制链接功能将不会起作用 30 | 31 | ``` 32 | docker run -d -p 8080:8080 --env "PASSWORD=1123456" --env "SIGN_KEY=dsfdsfd" -v /home/lixiaofei/testdata:/usr/src/app/.wrangler/state/v3/r2 mrlee326/imagehosting 33 | 34 | // PASSWORD为访问密码,不设置的话则允许公开上传,SIGN_KEY为签名key,设置的复杂一些就行了 35 | ``` 36 | 37 | 38 | 以下是安装到CF Workers的方法 39 | 40 | ## 安装环境其他依赖 41 | 42 | nodejs > v16.13.0 43 | 44 | 45 | ### 克隆仓库 46 | 47 | ``` 48 | git clone https://github.com/lixiaofei123/image-hosting-cfworkers.git 49 | ``` 50 | 51 | ### 进入到目录 52 | 53 | ``` 54 | cd image-hosting-cfworkers 55 | ``` 56 | 57 | ### 安装依赖 58 | 59 | ``` 60 | npm install 61 | ``` 62 | 63 | ### 创建R2存储桶 64 | 65 | 66 | ``` 67 | node_modules/wrangler/bin/wrangler.js r2 bucket create myimages # myimages 换成自己想要的存储桶名字 68 | ``` 69 | 70 | 这一步中间可能会要求进行授权,具体是要求打开一个网页,然后在网页上进行授权流程。如果你使用本地的电脑直接进行的本安装步骤的话,那么这一步应该不会有什么问题。如果你是远程连接到某台服务器上安装的话,需要根据下面步骤进行操作。 a. 复制授权的url,在本地浏览器上打开,进行授权等流程。 b 授权完毕后,浏览器最终跳转到一个http://localhost:8976/oauth/callback/xxxx地址 (xxxx地址是我随便写的,以实际显示的为准),此时浏览器上显示无法访问此地址。 c 复制浏览器上无法访问的这个地址,在终端软件上另外打开一个远程服务器的新会话,直接执行 curl "{替换成复制的地址}",即可完成授权流程。 请注意,整个命令执行超时时间,如果显示超时失败,重新尝试即可 71 | 72 | 73 | ### 编辑wrangler.toml配置文件 74 | 75 | 修改下面几个参数 76 | 77 | a. bucket_name = "myimages" 将其设置为第4步中创建的存储桶的名字 78 | 79 | b. R2_DOMAIN = "" 如果你想让图床直接返回R2对象存储的公开地址,可以设置这个R2_DOMAIN的值(需要在Cloudflare的R2对象存储中设置公开访问)。如果不设置,返回的地址是Workers代理后的地址。如果设置R2_DOMAIN,请注意CF R2的B类操作的免费额度为每月1000万次,超出就会扣费,每次访问图片都会消耗一次额度。Workers代理R2虽然也会产生B类操作,但是Workers一方面会使用缓存,另外一方面Workers的免费额度远小于B类操作的免费额度,且超出后只会暂停服务不会继续扣费。 80 | 81 | c. PASSWORD = "" 如果不设置,则允许匿名上传。(建议设置,Cloudflare提供的免费额度有限,自己使用即可) 82 | 83 | d. SIGN_KEY = "" 如果设置了PASSWORD,这个也需要设置,随便填写一串足够复杂的字符串即可 84 | 85 | ### 页面个性化(可选) 86 | 87 | 编辑 src/index.html,在第7行修改网页的标题,在第10行修改图床的背景图,在第11行修改登录框的头像,在第14行修改qrcode.min.js资源的地址。默认的资源地址都是来自我自用的图床,可以直接使用,但不保证一直有效。可以直接把相关的资源在Cloudflare的R2控制台上传完毕后,然后在这里用图床的地址进行引用。地址的格式为 {图床域名}/file/{资源路径} 88 | 89 | 90 | ### 部署 91 | 92 | ``` 93 | node_modules/wrangler/bin/wrangler.js deploy 94 | ``` 95 | 96 | 97 | 部署完毕后,即可以访问。由于CF Workers提供的默认域名在国内可能无法访问,建议在CF Workers的后台绑定自己的域名(在CF Workers的 设置 -> 触发器)。 98 | 99 | ### 打开地址访问 100 | 101 | 安装完成 102 | 103 | 104 | ## API接口 105 | 106 | 下面的(需鉴权)指的是在设置密码的情况下,授权方式有下面两种 107 | 108 | 1. Basic Auth 这种方式下,需要将 "admin:{密码}"用Base64密码编码后,通过Header或者Cookies将 Authentication: Basic {编码字符串} 传递给服务 109 | 110 | 例如 Authentication: Basic xxxxxxxxxxxxxxxxxxxxxxxx 111 | 112 | 2. Bearer Auth 这种方式下,需要先调用登录接口接口,登录接口会返回一个包含token的字符串,通过Header或者Cookies将 Authentication: Bearer {token} 传递给服务 113 | 114 | 例如 Authentication: Bearer xxxxxxxxxxxxxxxxxxxxxxxx 115 | 116 | 117 | ### 鉴权接口(需鉴权) 118 | 119 | POST /auth/check 120 | 121 | 返回的状态码如果是200代表鉴权成功,否则就是鉴权失败 122 | 123 | ### 登录接口 124 | 125 | POST /auth/login 126 | 127 | 参数: 128 | 129 | ``` 130 | { 131 | "password": "123456" 132 | } 133 | ``` 134 | 135 | 登录成功返回 136 | 137 | ``` 138 | { 139 | "token": "xxxxxx" 140 | } 141 | ``` 142 | 失败返回其它内容 143 | 144 | ### 上传接口(需鉴权) 145 | 146 | PUT /file/{key} 147 | 148 | body为要上传的文件,上传成功返回 149 | 150 | ``` 151 | { 152 | "url": "文件访问链接" 153 | } 154 | ``` 155 | 156 | 注意,同名的{key}会被覆盖,上传接口不检查文件的类型,即可以上传任何类型的文件 157 | 158 | ### 下载接口 159 | 160 | GET /file/{key} 161 | 162 | ### 删除接口(需鉴权) 163 | 164 | DELETE /file/{key} 165 | 166 | ### 远程获取接口(需鉴权) 167 | 168 | POST /fetchUrl/{资源URL} 169 | 170 | 从提供的资源URL中获取数据并进行保存,上传成功返回 171 | 172 | ``` 173 | { 174 | "url": "文件访问链接" 175 | } 176 | ``` 177 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import index from './index.html'; 2 | import { parse } from "cookie"; 3 | import jwt from '@tsndr/cloudflare-worker-jwt' 4 | import { Buffer } from "node:buffer"; 5 | 6 | interface Env { 7 | MY_BUCKET: R2Bucket; 8 | R2_DOMAIN: String; 9 | PASSWORD: string; 10 | SIGN_KEY: string; 11 | 12 | } 13 | 14 | async function checkAuth(req: Request, signKey: string, password: string) { 15 | const cookie = parse(req.headers.get("Cookie") || ""); 16 | const authentication = cookie["Authentication"] || req.headers.get("authentication") || "" 17 | if (authentication !== "") { 18 | if (authentication.indexOf("Basic") === 0) { 19 | const encodedstr = authentication.substring(6) 20 | const decodedstr = Buffer.from(encodedstr, 'base64').toString('ascii'); 21 | const userpass = decodedstr.split(":") 22 | return (userpass.length === 2 && userpass[1] === password) 23 | } else if (authentication.indexOf("Bearer") === 0) { 24 | let jwttoken = authentication.substring(7) 25 | return await jwt.verify(jwttoken, signKey) 26 | } 27 | } 28 | return false 29 | } 30 | 31 | async function handlePutObject(request: Request, key: string, data: ReadableStream | Blob | ArrayBuffer | null, env: Env): Promise { 32 | await env.MY_BUCKET.put(key, data); 33 | const url = new URL(request.url); 34 | 35 | let fileurl = `${url.protocol}//${url.host}/file/${key}` 36 | if (env.R2_DOMAIN) { 37 | fileurl = `${env.R2_DOMAIN}/${key}`; 38 | } 39 | return Response.json({ 40 | url: fileurl, 41 | }); 42 | } 43 | 44 | async function handleGetObject(_request: Request, key: string, env: Env): Promise { 45 | const object = await env.MY_BUCKET.get(key); 46 | if (object === null) { 47 | throw new Error('Object Not Found'); 48 | } 49 | const headers = new Headers(); 50 | object.writeHttpMetadata(headers); 51 | headers.set('etag', object.httpEtag); 52 | headers.set('Cache-Control', "public,max-age=2592000"); 53 | 54 | const response = new Response(object.body, { 55 | headers, 56 | }); 57 | 58 | return response; 59 | } 60 | 61 | async function handleDeleteObject(_request: Request, key: string, env: Env): Promise { 62 | await env.MY_BUCKET.delete(key); 63 | return Response.json({}); 64 | } 65 | 66 | async function handleObject(request: Request, env: Env, context: ExecutionContext): Promise { 67 | try { 68 | const url = new URL(request.url); 69 | const key = url.pathname.substring(url.pathname.indexOf('/file/') + 6); 70 | 71 | switch (request.method) { 72 | case 'PUT': 73 | return await handlePutObject(request, key, request.body, env); 74 | case 'GET': 75 | const cacheKey = new URL(request.url); 76 | const cache = caches.default; 77 | let response = await cache.match(cacheKey); 78 | if (response) { 79 | console.log(`Cache hit for: ${request.url}.`); 80 | return response; 81 | } 82 | 83 | response = await handleGetObject(request, key, env); 84 | 85 | let cacheResponse = response.clone(); 86 | cacheResponse.headers.set('Cache', 'true'); 87 | 88 | context.waitUntil(cache.put(cacheKey, cacheResponse)); 89 | return response; 90 | 91 | case 'DELETE': 92 | return await handleDeleteObject(request, key, env); 93 | 94 | default: 95 | throw new Error('Method is not allowed'); 96 | } 97 | } catch (error: any) { 98 | return Response.json( 99 | { 100 | error: error.message, 101 | }, 102 | { status: 400 } 103 | ); 104 | } 105 | } 106 | 107 | async function fetchUrl(request: Request, url: string) { 108 | const proxyurl = new URL(url); 109 | let header = new Headers(request.headers); 110 | header.set('Host', proxyurl.host); 111 | 112 | try { 113 | const resp = await fetch(proxyurl, { 114 | method: 'GET', 115 | headers: header, 116 | redirect: 'manual', 117 | }); 118 | let respHeaders = new Headers(resp.headers); 119 | if (respHeaders.has('location')) { 120 | const newurl = respHeaders.get('location') || ''; 121 | return await fetchUrl(request, newurl); 122 | } 123 | 124 | if (resp.status === 200) { 125 | const body = await resp.arrayBuffer() 126 | return body; 127 | } 128 | 129 | throw new Error("Fetch rrror") 130 | 131 | } catch (error) { 132 | throw error 133 | } 134 | } 135 | 136 | async function handleFetchUrl(request: Request, env: Env): Promise { 137 | const url = new URL(request.url); 138 | const pathname = url.pathname; 139 | const proxyurl = pathname.substring(pathname.indexOf('/fetchUrl/') + 10); 140 | 141 | try { 142 | const key = new URL(proxyurl).pathname.substring(1); 143 | const data = await fetchUrl(request, proxyurl) 144 | return await handlePutObject(request, key, data, env) 145 | 146 | } catch (error: any) { 147 | return Response.json({ 148 | error: error.message, 149 | }, { 150 | status: 500 151 | }) 152 | } 153 | } 154 | 155 | async function handleIndex(_request: Request, _env: Env): Promise { 156 | return new Response(index, { 157 | headers: { 158 | 'content-type': 'text/html', 159 | }, 160 | }); 161 | } 162 | 163 | const authRoutes = [ 164 | { 165 | prefix: "/file/", 166 | methods: "PUT,DELETE" 167 | },{ 168 | prefix: "/fetchUrl/", 169 | methods: "POST" 170 | }, 171 | { 172 | prefix: "/auth/check", 173 | methods: "POST" 174 | } 175 | ] 176 | 177 | async function authMiddleware(request: Request, env: Env) { 178 | const PASSWORD = (env.PASSWORD || "") + "" 179 | 180 | if(PASSWORD === ""){ 181 | return true 182 | } 183 | 184 | const SIGN_KEY = (env.SIGN_KEY || "abcdefg") + "" 185 | const url = new URL(request.url); 186 | const pathname = url.pathname; 187 | const method = request.method 188 | 189 | for(let i = 0; i < authRoutes.length; i++){ 190 | const prefix = authRoutes[i].prefix 191 | const methods = authRoutes[i].methods 192 | if(pathname.indexOf(prefix) === 0 && methods.split(",").some(i => i === method)){ 193 | return await checkAuth(request, SIGN_KEY, PASSWORD) 194 | } 195 | } 196 | 197 | return true 198 | } 199 | 200 | export default { 201 | async fetch(request: Request, env: Env, context: ExecutionContext): Promise { 202 | 203 | const url = new URL(request.url); 204 | const pathname = url.pathname; 205 | 206 | if(await authMiddleware(request, env) === false){ 207 | return new Response("鉴权失败", { status: 401 }) 208 | } 209 | 210 | if (pathname === '' || pathname === '/') { 211 | return await handleIndex(request, env); 212 | } 213 | 214 | if (pathname.indexOf('/file/') === 0) { 215 | return await handleObject(request, env, context); 216 | } 217 | 218 | if (pathname.indexOf('/fetchUrl/') === 0 && request.method === "POST") { 219 | return handleFetchUrl(request, env) 220 | } 221 | 222 | if(pathname === "/auth/login" && request.method === "POST"){ 223 | const PASSWORD = (env.PASSWORD || "") + "" 224 | const SIGN_KEY = (env.SIGN_KEY || "abcdefg") + "" 225 | 226 | if (PASSWORD !== "") { 227 | const bodyjson: any = await request.json() 228 | const pwd: string = bodyjson["password"] 229 | if (pwd === PASSWORD) { 230 | const authentication = await jwt.sign({ exp: Math.floor(Date.now() / 1000 + 60 * 60 * 24 * 30) }, SIGN_KEY) 231 | return Response.json({ 232 | authentication: authentication 233 | }) 234 | } else { 235 | return new Response("密码错误", { status: 401 }) 236 | } 237 | } else { 238 | return Response.json({}) 239 | } 240 | } 241 | 242 | if (url.pathname === "/auth/check" && request.method === "POST") { 243 | return new Response("", { status: 200 }) 244 | } 245 | 246 | return Response.json( 247 | { 248 | error: 'Unknown error', 249 | }, 250 | { status: 400 } 251 | ); 252 | }, 253 | } satisfies ExportedHandler; 254 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | "lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, 16 | "jsx": "react" /* Specify what JSX code is generated. */, 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "es2022" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | "types": [ 35 | "@cloudflare/workers-types/2023-07-01" 36 | ] /* Specify type package names to be included without being referenced in a source file. */, 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | "resolveJsonModule": true /* Enable importing .json files */, 39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, 43 | "checkJs": false /* Enable error reporting in type-checked JavaScript files. */, 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | "noEmit": true /* Disable emitting files from a compilation. */, 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, 73 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 74 | // "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 81 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 86 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "exclude": ["test"] 104 | } 105 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 灼灼其华 8 | 14 | 15 | 320 | 321 | 322 | 323 |
324 |
325 | 328 |
329 | 330 |
331 |
332 |
333 |
334 |
上传并分享你的图片
335 |
将图片拖拽到页面的任意区域或者点击下面的上传按钮或者使用粘贴截图和图片链接来上传您的图片
336 | 337 |
338 |
339 |
340 | 341 |
342 |
343 | 344 |
345 |
346 |
347 | 348 | 352 | 353 |
354 |
355 |
356 | 357 |
358 |
请输入访问口令
359 |
360 | 361 |
362 | 364 | 366 | 367 |
368 |
369 |
370 |
371 | 374 | 375 | 689 | 690 | 691 | 692 | --------------------------------------------------------------------------------