Skip to content

Instantly share code, notes, and snippets.

@aabccd021
Created July 26, 2024 13:44
Show Gist options
  • Save aabccd021/a7581efb57b6d37c67a822b61ac8039d to your computer and use it in GitHub Desktop.
Save aabccd021/a7581efb57b6d37c67a822b61ac8039d to your computer and use it in GitHub Desktop.
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as path from 'node:path';
import * as cp from 'node:child_process';
import { parseArgs } from 'node:util';
import { z } from 'zod';
import type { Auth } from 'googleapis';
import { google } from 'googleapis';
import opn from 'open';
const autoCloseHtml = `
<!DOCTYPE html>
<html>
<head>
<script>
window.close();
</script>
</head>
</html>
`;
const waitForRequest = (port: number): Promise<string | undefined> =>
new Promise((resolve, reject) => {
const server = http
.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(autoCloseHtml, () => {
server.close();
resolve(req.url);
});
})
.listen(port)
.on('error', reject);
});
const getAuthCode = async (
auth: Auth.OAuth2Client,
scopes: readonly string[],
redirectUri: string
): Promise<string> => {
const authorizeUrl = auth.generateAuthUrl({
access_type: 'offline',
scope: scopes.join(' '),
});
console.log(
`Opening on browser. ` +
`If the browser does not open, please open the following URL manually. ${authorizeUrl}`
);
const port = parseInt(new URL(redirectUri).port);
const [requestUrl, opendedProcess] = await Promise.all([
waitForRequest(port),
opn(authorizeUrl, { wait: false }),
]);
opendedProcess.unref();
if (requestUrl === undefined) {
throw new Error('req.url is undefined');
}
const code = new URL(requestUrl, redirectUri).searchParams.get('code');
if (code === null) {
throw new Error('code is null');
}
return code;
};
const credentialsDirPath = './credentials';
const clientSecretFilename = 'client_secret.json';
const clientSecretPath = `${credentialsDirPath}/${clientSecretFilename}`;
const projectName = "private-management-aabccd021";
const clientSecretJsonNotFoundError = `
${clientSecretPath} is not found.
Download it from https://console.cloud.google.com/apis/credentials?project=${projectName} and put it in the current directory.
If you can't find it on console, create one by following the steps below or here https://github.com/googleapis/google-api-nodejs-client#oauth2-client.
- Navigate to the Cloud Console and [Create a new OAuth2 Client Id](https://console.cloud.google.com/apis/credentials/oauthclient)
- Select "Web Application" for the application type
- Add an authorized redirect URI with the value "http://localhost:3000/oauth2callback" (or applicable value for your scenario)
- Click "Create", and "Ok" on the following screen
- Click the "Download" icon next to your newly created OAuth2 Client Id
Make sure to store this file in safe place, and **do not check this file into source control!**
Also, don't forget to add test user on https://console.cloud.google.com/apis/credentials/consent?project=${projectName}
`;
const scopeToFilename = (
scopes: readonly string[]
) => {
const scopesHash = crypto.createHash('sha256').update(scopes.join(' ')).digest('hex');
return `${scopesHash}.credential.json`;
}
const credentialsSchema =
z.object({
refresh_token: z.string(),
expiry_date: z.number(),
access_token: z.string(),
token_type: z.string(),
id_token: z.string(),
scope: z.string(),
}).partial()
const getCredentialsOnLocalMachine = async (
auth: Auth.OAuth2Client,
scopes: readonly string[],
redirectUri: string
): Promise<Auth.Credentials> => {
const scopeFilename = scopeToFilename(scopes);
const credentialPath = `${credentialsDirPath}/${scopeFilename}`;
if (fs.existsSync(credentialPath)) {
const credentialsJsonFile = await fs.promises.readFile(credentialPath, 'utf-8')
const credentialsJson: unknown = JSON.parse(credentialsJsonFile);
const credentials = await credentialsSchema.parseAsync(credentialsJson);
if (credentials.refresh_token === undefined) {
await fs.promises.rm(credentialPath);
throw new Error('please revoke app access at ' +
'https://myaccount.google.com/connections?continue=https%3A%2F%2Fmyaccount.google.com%2Fdata-and-privacy ' +
'and re-run this script'
)
}
return credentials;
}
const authCode = await getAuthCode(auth, scopes, redirectUri);
const { tokens } = await auth.getToken(authCode);
await fs.promises.writeFile(credentialPath, JSON.stringify(tokens, null, 2));
return tokens;
};
const ghaCredentialsEnvKey = 'GOOGLE_API_CREDENTIALS'
const ghaCredentialsRecord = async () => {
const credentialsStr = process.env[ghaCredentialsEnvKey];
if (credentialsStr === undefined) {
throw new Error(`process.env.${ghaCredentialsEnvKey} is undefined`);
}
return z.record(z.unknown()).parseAsync(JSON.parse(credentialsStr))
}
const getCredentialsOnGha = async (
scopes: readonly string[],
): Promise<Auth.Credentials> => {
const credentialsRecord = await ghaCredentialsRecord();
const scopeFilename = scopeToFilename(scopes);
const credential = credentialsRecord[scopeFilename];
return await credentialsSchema.parseAsync(credential);
};
const clientSecretSchema = z.object({
web: z.object({
client_id: z.string(),
client_secret: z.string(),
redirect_uris: z.array(z.string()),
}),
});
const getAllCredentials = async () => {
const fileNames = await fs.promises.readdir(credentialsDirPath);
const promises = fileNames.map(async (fileName) => {
const filePath = path.join(credentialsDirPath, fileName);
const fileContent = await fs.promises.readFile(filePath, 'utf-8');
return [fileName, JSON.parse(fileContent)];
});
const fileEntries = await Promise.all(promises);
return Object.fromEntries(fileEntries);
}
const isLocalMachine = () => fs.existsSync(credentialsDirPath);
const getCredentials = async (
auth: Auth.OAuth2Client,
scopes: readonly string[],
redirectUri: string
): Promise<Auth.Credentials> => {
if (!isLocalMachine()) {
return await getCredentialsOnGha(scopes);
}
const credentials = await getCredentialsOnLocalMachine(auth, scopes, redirectUri);
const allCredentials = await getAllCredentials();
const envVarKey = `__PIVATE_REPOSITORY_GH_SECRET_SET__`;
cp.execSync(`gh secret set ${ghaCredentialsEnvKey} --body "$${envVarKey}"`, {
stdio: 'inherit',
env: {
...process.env,
[envVarKey]: JSON.stringify(allCredentials),
},
})
return credentials;
}
const getClientSecretJson = async () => {
if (!isLocalMachine()) {
const credentialsRecord = await ghaCredentialsRecord();
const clientSecretJsonStr = credentialsRecord[clientSecretFilename];
if (clientSecretJsonStr === undefined) {
throw new Error(`client_secret.json is not found in ${ghaCredentialsEnvKey}`);
}
return clientSecretJsonStr;
}
if (!fs.existsSync(clientSecretPath)) {
throw new Error(clientSecretJsonNotFoundError);
}
const jsonStr = await fs.promises.readFile(clientSecretPath, 'utf-8');
return JSON.parse(jsonStr);
}
const getAuth = async (scopes: readonly string[]): Promise<Auth.OAuth2Client> => {
const clientSecretJson: unknown = await getClientSecretJson();
const clientSecret = clientSecretSchema.parse(clientSecretJson).web;
const [redirectUri] = clientSecret.redirect_uris;
if (redirectUri === undefined) {
throw new Error(`redirect_uris in ${clientSecretPath} is empty`);
}
const auth = new google.auth.OAuth2(
clientSecret.client_id,
clientSecret.client_secret,
redirectUri
);
const credentials = await getCredentials(auth, scopes, redirectUri);
auth.setCredentials(credentials);
return auth;
};
const { values: args } = parseArgs({
options: {
path: {
type: 'string',
short: 'p',
},
},
});
const main = async () => {
const filePath = args.path;
if (filePath === undefined) {
throw new Error('path is required');
}
const auth = await getAuth(['https://www.googleapis.com/auth/drive']);
console.log(`Uploading ${filePath}`)
await google
.drive({ version: 'v3', auth })
.files
.create({
requestBody: {
name: path.basename(filePath)
},
media: {
body: fs.createReadStream(filePath),
}
});
console.log(`Uploaded ${filePath}`)
}
void main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment