mirror of
https://github.com/Nezumi-2711/google-drive-s3.git
synced 2026-09-22 13:38:30 +00:00
fix: add script for get refresh token
This commit is contained in:
@@ -18,10 +18,15 @@ For the sake of your Google account, we strongly recommend using this in an envi
|
|||||||
## How to Deploy
|
## How to Deploy
|
||||||
|
|
||||||
### 1. Prepare a Google Drive API Refresh Token
|
### 1. Prepare a Google Drive API Refresh Token
|
||||||
You will need to use rclone to obtain your Google API credentials and a Google Drive API refresh token.
|
You need Google API credentials and a Google Drive API refresh token. Either method below requires you to first create your own OAuth client (Google requires this per-app; a shared client cannot be scripted around it) — see the steps in either option.
|
||||||
|
|
||||||
|
**Option A: rclone**
|
||||||
Follow the rclone documentation to configure the client.
|
Follow the rclone documentation to configure the client.
|
||||||
https://rclone.org/drive/#making-your-own-client-id
|
https://rclone.org/drive/#making-your-own-client-id
|
||||||
|
|
||||||
|
**Option B: local script**
|
||||||
|
Run `pnpm get-refresh-token -- --client-id <ID> --client-secret <SECRET>` (see `scripts/get-google-refresh-token.mjs` for the Google Cloud Console setup steps — enabling the Drive API and creating a "Desktop app" OAuth client). It opens the consent screen, catches the redirect locally, and prints the values below directly.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> When your Google API client is in "Testing" mode, the refresh token will expire after a certain period of time, so if you need to use it for a long period of time, be sure to switch the mode before authenticating with rclone.
|
> When your Google API client is in "Testing" mode, the refresh token will expire after a certain period of time, so if you need to use it for a long period of time, be sure to switch the mode before authenticating with rclone.
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"start": "pnpx wrangler dev",
|
"start": "pnpx wrangler dev",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"cf-typegen": "pnpx wrangler types",
|
"cf-typegen": "pnpx wrangler types",
|
||||||
|
"get-refresh-token": "node scripts/get-google-refresh-token.mjs",
|
||||||
"lint": "biome check",
|
"lint": "biome check",
|
||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Local alternative to `rclone config` for obtaining a Google Drive OAuth
|
||||||
|
// refresh token. Runs the standard "installed app" / loopback OAuth flow
|
||||||
|
// (RFC 8252): opens the consent screen, catches the redirect on a local
|
||||||
|
// HTTP server, and exchanges the code for tokens.
|
||||||
|
//
|
||||||
|
// You still need your own OAuth client (Google requires this per-app, it
|
||||||
|
// cannot be generated by a script):
|
||||||
|
// 1. https://console.cloud.google.com/ -> create/select a project.
|
||||||
|
// 2. APIs & Services > Library -> enable "Google Drive API".
|
||||||
|
// 3. APIs & Services > OAuth consent screen -> External, Testing is fine;
|
||||||
|
// add your own Google account under "Test users".
|
||||||
|
// 4. APIs & Services > Credentials -> Create credentials > OAuth client ID
|
||||||
|
// -> Application type: "Desktop app". Copy the Client ID and Client secret.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// node scripts/get-google-refresh-token.mjs --client-id ID --client-secret SECRET
|
||||||
|
// (or set GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET env vars instead of flags)
|
||||||
|
//
|
||||||
|
// Optional: --port 8976 (must not be in use; Desktop-app clients accept any
|
||||||
|
// loopback port automatically, no need to register it in Google Cloud Console).
|
||||||
|
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import http from "node:http";
|
||||||
|
import { exec } from "node:child_process";
|
||||||
|
import { URL } from "node:url";
|
||||||
|
|
||||||
|
const SCOPE = "https://www.googleapis.com/auth/drive";
|
||||||
|
const AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||||
|
const TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
||||||
|
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const opts = { port: 8976 };
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = args[i];
|
||||||
|
if (a === "--client-id") opts.clientId = args[++i];
|
||||||
|
else if (a === "--client-secret") opts.clientSecret = args[++i];
|
||||||
|
else if (a === "--port") opts.port = Number(args[++i]);
|
||||||
|
else if (a === "--help" || a === "-h") opts.help = true;
|
||||||
|
}
|
||||||
|
opts.clientId ??= process.env.GOOGLE_CLIENT_ID;
|
||||||
|
opts.clientSecret ??= process.env.GOOGLE_CLIENT_SECRET;
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printHelp() {
|
||||||
|
console.log(`
|
||||||
|
Usage: node scripts/get-google-refresh-token.mjs --client-id <ID> --client-secret <SECRET> [--port 8976]
|
||||||
|
|
||||||
|
Before running this script, create a Desktop-app OAuth client in the Google
|
||||||
|
Cloud Console (see the comment block at the top of this file for the exact
|
||||||
|
steps) and enable the Google Drive API for that project.
|
||||||
|
|
||||||
|
Env vars GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are accepted instead of flags.
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openBrowser(url) {
|
||||||
|
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
||||||
|
exec(cmd, () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const opts = parseArgs();
|
||||||
|
if (opts.help || !opts.clientId || !opts.clientSecret) {
|
||||||
|
printHelp();
|
||||||
|
process.exit(opts.help ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUri = `http://127.0.0.1:${opts.port}/`;
|
||||||
|
const state = randomBytes(16).toString("hex");
|
||||||
|
|
||||||
|
const authUrl = new URL(AUTH_ENDPOINT);
|
||||||
|
authUrl.searchParams.set("client_id", opts.clientId);
|
||||||
|
authUrl.searchParams.set("redirect_uri", redirectUri);
|
||||||
|
authUrl.searchParams.set("response_type", "code");
|
||||||
|
authUrl.searchParams.set("scope", SCOPE);
|
||||||
|
authUrl.searchParams.set("access_type", "offline");
|
||||||
|
authUrl.searchParams.set("prompt", "consent"); // force refresh_token even on repeat auth
|
||||||
|
authUrl.searchParams.set("state", state);
|
||||||
|
|
||||||
|
const code = await new Promise((resolve, reject) => {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
const reqUrl = new URL(req.url, redirectUri);
|
||||||
|
if (reqUrl.pathname !== "/") {
|
||||||
|
res.writeHead(404).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnedState = reqUrl.searchParams.get("state");
|
||||||
|
const err = reqUrl.searchParams.get("error");
|
||||||
|
const authCode = reqUrl.searchParams.get("code");
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(400, { "Content-Type": "text/plain" }).end(`Authorization failed: ${err}`);
|
||||||
|
server.close();
|
||||||
|
reject(new Error(`Google returned error: ${err}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (returnedState !== state || !authCode) {
|
||||||
|
res.writeHead(400, { "Content-Type": "text/plain" }).end("Invalid state or missing code.");
|
||||||
|
server.close();
|
||||||
|
reject(new Error("State mismatch or missing authorization code."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { "Content-Type": "text/plain" }).end("Authorization complete. You can close this tab and return to the terminal.");
|
||||||
|
server.close();
|
||||||
|
resolve(authCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(opts.port, "127.0.0.1", () => {
|
||||||
|
console.log(`\nOpen this URL in your browser if it doesn't open automatically:\n\n${authUrl.toString()}\n`);
|
||||||
|
openBrowser(authUrl.toString());
|
||||||
|
console.log("Waiting for authorization...");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenRes = await fetch(TOKEN_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
code,
|
||||||
|
client_id: opts.clientId,
|
||||||
|
client_secret: opts.clientSecret,
|
||||||
|
redirect_uri: redirectUri,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenData = await tokenRes.json();
|
||||||
|
if (!tokenRes.ok) {
|
||||||
|
console.error("Token exchange failed:", tokenData);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!tokenData.refresh_token) {
|
||||||
|
console.error(
|
||||||
|
"No refresh_token was returned. This usually means the account already has an active grant.\n" +
|
||||||
|
"Revoke it at https://myaccount.google.com/permissions and re-run this script.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nSuccess. Set these as your Worker secrets:\n");
|
||||||
|
console.log(`GOOGLE_CLIENT_ID=${opts.clientId}`);
|
||||||
|
console.log(`GOOGLE_CLIENT_SECRET=${opts.clientSecret}`);
|
||||||
|
console.log(`GOOGLE_REFRESH_TOKEN=${tokenData.refresh_token}`);
|
||||||
|
console.log("\nExample (from the repo root):\n");
|
||||||
|
console.log(` echo -n "${opts.clientId}" | npx wrangler secret put GOOGLE_CLIENT_ID`);
|
||||||
|
console.log(` echo -n "${opts.clientSecret}" | npx wrangler secret put GOOGLE_CLIENT_SECRET`);
|
||||||
|
console.log(` echo -n "${tokenData.refresh_token}" | npx wrangler secret put GOOGLE_REFRESH_TOKEN`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err.message ?? err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+46
-44
@@ -3,67 +3,69 @@
|
|||||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
"$schema": "node_modules/wrangler/config-schema.json",
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
"name": "iris",
|
"name": "iris",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"compatibility_date": "2025-09-27",
|
"compatibility_date": "2025-09-27",
|
||||||
"vars": {
|
"vars": {
|
||||||
"ALLOW_MULTIPART": "false",
|
"ALLOW_MULTIPART": "true",
|
||||||
"ETAG_STYLE": "md5"
|
"ETAG_STYLE": "md5"
|
||||||
},
|
},
|
||||||
"observability": {
|
"observability": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"durable_objects": {
|
"durable_objects": {
|
||||||
"bindings": [
|
"bindings": [
|
||||||
{
|
{
|
||||||
"name": "MPU",
|
"name": "MPU",
|
||||||
"class_name": "MultipartUploadDO"
|
"class_name": "MultipartUploadDO"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"migrations": [
|
"migrations": [
|
||||||
{
|
{
|
||||||
"tag": "v1",
|
"tag": "v1",
|
||||||
"new_sqlite_classes": ["MultipartUploadDO"]
|
"new_sqlite_classes": [
|
||||||
}
|
"MultipartUploadDO"
|
||||||
],
|
]
|
||||||
"kv_namespaces": [
|
}
|
||||||
{
|
],
|
||||||
"binding": "AUTH_KV",
|
"kv_namespaces": [
|
||||||
"id": "ba953a1ce2cd41a7ba1d348075d34171"
|
{
|
||||||
},
|
"binding": "AUTH_KV",
|
||||||
{
|
"id": "2113be46ce514e088572d0ef7459c1bf"
|
||||||
"binding": "FOLDER_CACHE",
|
},
|
||||||
"id": "6332b0674a8342e4b270ca3368d3c52a"
|
{
|
||||||
}
|
"binding": "FOLDER_CACHE",
|
||||||
]
|
"id": "6d2f9904a3ff4471b0f04025c1ae87cd"
|
||||||
/**
|
}
|
||||||
|
]
|
||||||
|
/**
|
||||||
* Smart Placement
|
* Smart Placement
|
||||||
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||||
*/
|
*/
|
||||||
// "placement": { "mode": "smart" }
|
// "placement": { "mode": "smart" }
|
||||||
/**
|
/**
|
||||||
* Bindings
|
* Bindings
|
||||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||||
* databases, object storage, AI inference, real-time communication and more.
|
* databases, object storage, AI inference, real-time communication and more.
|
||||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Environment Variables
|
* Environment Variables
|
||||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||||
* Note: Use secrets to store sensitive data.
|
* Note: Use secrets to store sensitive data.
|
||||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||||
*/
|
*/
|
||||||
// "vars": { "MY_VARIABLE": "production_value" }
|
// "vars": { "MY_VARIABLE": "production_value" }
|
||||||
/**
|
/**
|
||||||
* Static Assets
|
* Static Assets
|
||||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||||
*/
|
*/
|
||||||
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||||
/**
|
/**
|
||||||
* Service Bindings (communicate between multiple Workers)
|
* Service Bindings (communicate between multiple Workers)
|
||||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
* https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings
|
||||||
*/
|
*/
|
||||||
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
|
// "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user