mirror of
https://github.com/Nezumi-2711/google-drive-s3.git
synced 2026-09-22 05:31:47 +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
|
||||
|
||||
### 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.
|
||||
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]
|
||||
> 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",
|
||||
"test": "vitest --run",
|
||||
"cf-typegen": "pnpx wrangler types",
|
||||
"get-refresh-token": "node scripts/get-google-refresh-token.mjs",
|
||||
"lint": "biome check",
|
||||
"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);
|
||||
});
|
||||
+47
-45
@@ -3,67 +3,69 @@
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
*/
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "iris",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-09-27",
|
||||
"vars": {
|
||||
"ALLOW_MULTIPART": "false",
|
||||
"ETAG_STYLE": "md5"
|
||||
},
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "MPU",
|
||||
"class_name": "MultipartUploadDO"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": ["MultipartUploadDO"]
|
||||
}
|
||||
],
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "AUTH_KV",
|
||||
"id": "ba953a1ce2cd41a7ba1d348075d34171"
|
||||
},
|
||||
{
|
||||
"binding": "FOLDER_CACHE",
|
||||
"id": "6332b0674a8342e4b270ca3368d3c52a"
|
||||
}
|
||||
]
|
||||
/**
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "iris",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-09-27",
|
||||
"vars": {
|
||||
"ALLOW_MULTIPART": "true",
|
||||
"ETAG_STYLE": "md5"
|
||||
},
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"durable_objects": {
|
||||
"bindings": [
|
||||
{
|
||||
"name": "MPU",
|
||||
"class_name": "MultipartUploadDO"
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrations": [
|
||||
{
|
||||
"tag": "v1",
|
||||
"new_sqlite_classes": [
|
||||
"MultipartUploadDO"
|
||||
]
|
||||
}
|
||||
],
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "AUTH_KV",
|
||||
"id": "2113be46ce514e088572d0ef7459c1bf"
|
||||
},
|
||||
{
|
||||
"binding": "FOLDER_CACHE",
|
||||
"id": "6d2f9904a3ff4471b0f04025c1ae87cd"
|
||||
}
|
||||
]
|
||||
/**
|
||||
* Smart Placement
|
||||
* https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
|
||||
*/
|
||||
// "placement": { "mode": "smart" }
|
||||
/**
|
||||
// "placement": { "mode": "smart" }
|
||||
/**
|
||||
* Bindings
|
||||
* Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including
|
||||
* databases, object storage, AI inference, real-time communication and more.
|
||||
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
||||
*/
|
||||
/**
|
||||
/**
|
||||
* Environment Variables
|
||||
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
|
||||
* Note: Use secrets to store sensitive data.
|
||||
* https://developers.cloudflare.com/workers/configuration/secrets/
|
||||
*/
|
||||
// "vars": { "MY_VARIABLE": "production_value" }
|
||||
/**
|
||||
// "vars": { "MY_VARIABLE": "production_value" }
|
||||
/**
|
||||
* Static Assets
|
||||
* https://developers.cloudflare.com/workers/static-assets/binding/
|
||||
*/
|
||||
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||
/**
|
||||
// "assets": { "directory": "./public/", "binding": "ASSETS" }
|
||||
/**
|
||||
* Service Bindings (communicate between multiple Workers)
|
||||
* 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