Created
July 26, 2024 13:44
-
-
Save aabccd021/a7581efb57b6d37c67a822b61ac8039d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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