使用github私有仓库和Cloudflare Workers搭建个人图床

之前一直在使用 GitHub 的公开仓库和 jsDelivr 代理作为个人图床,直到最近 jsDelivr 喜提认证无法使用。尽管还有其他的 CDN 代理可以使用,但是还是有被墙的风险,到时候修改图片链接就太麻烦了。如需稳定的使用图床,需要自己使用服务器对 GitHub 进行反代或购买云存储服务,增加了成本。近期研究后,发现了一个几乎零成本的解决方案,用作本人目前图床方案,推荐给大家。

该方案的主要思路是使用 Cloudflare 的 Workers 来代理 github 私有仓库中文件的地址,并绑定自己的域名进行使用。该方案主要优势是:

  1. github服务稳定,不会跑路。
  2. 相比有免费额度的存储服务-七牛云和backblaze等,github没有额度的限制。
  3. 使用的是 github 的私有仓库,存储里的文件列表并不会像公开仓库一样全部对外暴露,有一定的安全性。
  4. 使用自己的域名,方便以后可能的服务迁移。
  5. Cloudflare Workers每天有10万次的免费请求额度,正常使用不可能用完。

使用该方案你需要以下东西:

  1. 一个没有被墙的域名。在阿里云上购买.top域名,10年也才100多块。
  2. Cloudflare 账号。免费,直接注册即可。
  3. github 账号,也是免费。

创建 github 私有仓库并获取个人访问令牌

首先,在github上建一个私有仓库

1680186532176创建私有仓库.png

然后,在 GitHub 上生成一个 Personal access token(个人访问令牌),用于身份验证。在 GitHub 网站上登录账户,点击右上角用户头像,进入 Settings(设置)页面。在这个页面中左侧侧边栏选择 Developer settings(开发人员设置),然后点击 Personal access tokens(个人访问令牌)菜单里的 Token classic,点击 Generate new token 开始创建一个新的令牌,注意一定要选择 classic 方式。

创建github令牌直达链接

1680187477177创建令牌入口.png

在创建页面中,填写 Note 为“图床”,Expiration(过期时间)为 No expiration(永久),在下面的Select scopes(选择权限范围)如下图勾选 repo。最后点击 generate token 生成令牌即可。

1680187618175创建令牌.png

在生成后的页面中会看到新生成的github令牌,该令牌后面会使用到。

务必将令牌保存起来,放在一个安全的地方,页面关掉后就看不到了。

在 Cloudflare 上创建用于代理的 Worker

登录到 Cloudflare 的管理界面后,点击侧边栏的 “Workers” 选项,然后点击 “创建服务” 创建一个 Worker。

1680189326182创建worker入口.png

在创建服务界面中,填写“服务名称”,选择“HTTP 处理程序”,点击“创建服务”。

1680190069175创建worker入口2.png

在创建服务详情页中点击“快速编辑”。

1680190070178创建worker入口3.png

将下面的代码复制粘贴到编辑页面的代码编辑器中。

// Website you intended to retrieve for users.
const upstream = "raw.githubusercontent.com";

// Custom pathname for the upstream website.
// (1) 填写代理的路径,格式为 /<用户>/<仓库名>/<分支>
const upstream_path = "/yuanwen0327/assets/main";

// github personal access token.
// (2) 填写github令牌
const github_token = "";

// Website you intended to retrieve for users using mobile devices.
const upstream_mobile = upstream;

// Countries and regions where you wish to suspend your service.
const blocked_region = [];

// IP addresses which you wish to block from using your service.
const blocked_ip_address = ["0.0.0.0", "127.0.0.1"];

// Whether to use HTTPS protocol for upstream address.
const https = true;

// Whether to disable cache.
const disable_cache = false;

// Replace texts.
const replace_dict = {
  $upstream: "$custom_domain",
};

addEventListener("fetch", (event) => {
  event.respondWith(fetchAndApply(event.request));
});

async function fetchAndApply(request) {
  const region = request.headers.get("cf-ipcountry")?.toUpperCase();
  const ip_address = request.headers.get("cf-connecting-ip");
  const user_agent = request.headers.get("user-agent");

  let response = null;
  let url = new URL(request.url);
  let url_hostname = url.hostname;

  if (https == true) {
    url.protocol = "https:";
  } else {
    url.protocol = "http:";
  }

  if (await device_status(user_agent)) {
    var upstream_domain = upstream;
  } else {
    var upstream_domain = upstream_mobile;
  }

  url.host = upstream_domain;
  if (url.pathname == "/") {
    url.pathname = upstream_path;
  } else {
    url.pathname = upstream_path + url.pathname;
  }

  if (blocked_region.includes(region)) {
    response = new Response(
      "Access denied: WorkersProxy is not available in your region yet.",
      {
        status: 403,
      }
    );
  } else if (blocked_ip_address.includes(ip_address)) {
    response = new Response(
      "Access denied: Your IP address is blocked by WorkersProxy.",
      {
        status: 403,
      }
    );
  } else {
    let method = request.method;
    let request_headers = request.headers;
    let new_request_headers = new Headers(request_headers);

    new_request_headers.set("Host", upstream_domain);
    new_request_headers.set("Referer", url.protocol + "//" + url_hostname);
    new_request_headers.set("Authorization", "token " + github_token);

    let original_response = await fetch(url.href, {
      method: method,
      headers: new_request_headers,
      body: request.body,
    });

    connection_upgrade = new_request_headers.get("Upgrade");
    if (connection_upgrade && connection_upgrade.toLowerCase() == "websocket") {
      return original_response;
    }

    let original_response_clone = original_response.clone();
    let original_text = null;
    let response_headers = original_response.headers;
    let new_response_headers = new Headers(response_headers);
    let status = original_response.status;

    if (disable_cache) {
      new_response_headers.set("Cache-Control", "no-store");
    } else {
      new_response_headers.set("Cache-Control", "max-age=43200000");
    }

    new_response_headers.set("access-control-allow-origin", "*");
    new_response_headers.set("access-control-allow-credentials", true);
    new_response_headers.delete("content-security-policy");
    new_response_headers.delete("content-security-policy-report-only");
    new_response_headers.delete("clear-site-data");

    if (new_response_headers.get("x-pjax-url")) {
      new_response_headers.set(
        "x-pjax-url",
        response_headers
          .get("x-pjax-url")
          .replace("//" + upstream_domain, "//" + url_hostname)
      );
    }

    const content_type = new_response_headers.get("content-type");
    if (
      content_type != null &&
      content_type.includes("text/html") &&
      content_type.includes("UTF-8")
    ) {
      original_text = await replace_response_text(
        original_response_clone,
        upstream_domain,
        url_hostname
      );
    } else {
      original_text = original_response_clone.body;
    }

    response = new Response(original_text, {
      status,
      headers: new_response_headers,
    });
  }
  return response;
}

async function replace_response_text(response, upstream_domain, host_name) {
  let text = await response.text();

  var i, j;
  for (i in replace_dict) {
    j = replace_dict[i];
    if (i == "$upstream") {
      i = upstream_domain;
    } else if (i == "$custom_domain") {
      i = host_name;
    }

    if (j == "$upstream") {
      j = upstream_domain;
    } else if (j == "$custom_domain") {
      j = host_name;
    }

    let re = new RegExp(i, "g");
    text = text.replace(re, j);
  }
  return text;
}

async function device_status(user_agent_info) {
  var agents = [
    "Android",
    "iPhone",
    "SymbianOS",
    "Windows Phone",
    "iPad",
    "iPod",
  ];
  var flag = true;
  for (var v = 0; v < agents.length; v++) {
    if (user_agent_info.indexOf(agents[v]) > 0) {
      flag = false;
      break;
    }
  }
  return flag;
}

1680190068175部署worker.png

如图,有2个地方的代码需要自己修改一下。第一个是代理的路径,需要改写成自己的用户/仓库/分支,用户和仓库可以在github私有仓库页的url上看到,如https://github.com/yuanwen0327/assets,yuanwen0327是用户名,assets是仓库名,分支现在一般默认是main。第二是令牌,需要修改成之前github上申请的令牌。

大概讲一下,这串代码的作用:

  1. 反向代理了github仓库。
  2. 使用令牌获取文件。
  3. 开启了缓存,避免重复请求图片。

最后“保存并部署”,服务就部署成功了。Cloudflare 会自动给新创建的 Worker 服务分配域名,但是这个域名非常容易被墙,接下来需要给 Worker 绑定自己购买的域名。

将域名 NS 转到 Cloudflare

Cloudflare Workers 的域名绑定仅支持托管在 Cloudflare 上的域名,所以得先将域名的 NS 转到 Cloudflare。大概步骤如下:

  1. 单击菜单栏上的“添加站点”按钮,输入您的网站域名并单击“添加站点”。

  2. Cloudflare 会在 DNS 扫描期间列出您的网站 DNS 记录。单击下拉菜单中的“继续设置”。

  3. Cloudflare 会提示您选择服务计划。您可以选择“免费”或“收费”计划。您可以单击“继续”并在下一页上单击“继续设置”。

  4. 接下来,Cloudflare 会生成替换您原来的 DNS 记录的新 DNS 记录。请将这些新 DNS 记录复制并粘贴到您的域名注册商的 DNS 设置中。

  5. 在您的域名注册商的网站上,导航到域名管理区域,并找到 NS 记录设置。删除原来的 NS 记录,将它们替换为 Cloudflare 提供的新 NS 记录。

  6. 保存更改并等待 DNS 记录更新。这通常需要几小时。

给创建的 Worker 服务绑定自己的域名

域名 NS 转到 Cloudflare成功后,在 Worker 服务的详情页点击“触发器”,然后点击“添加自定义域”。

1680190063178绑定域名.png

输入想要绑定的域名后,点击“添加自定义域”。

1680190067175绑定域名2.png

到这里,Cloudflare的配置就完成了。下面需要配置图床软件和测试一下。

配置 picgo 图床软件

picgo下载地址

下载picgo并安装好后,按下图进行配置。仓库名为用户/仓库,分支为main,Token为保存的github令牌,存储路径我这里定义的是img/,域名为之前绑定的 Worker 服务的域名。

1680192323176picgo配置.png

保存并设为默认图床后,可以上传几张图片试一下,上传成功的图片路径应该是自己的域名+路径+文件名,如https://assets.yw3.fun/img/test.png

到这里就全部完成了,愉快的白piao吧🥳🥰🥰。

👉文章来源:使用github私有仓库和Cloudflare Workers搭建个人图床 - Devin's Blog (yw3.fun)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容