Last active
January 26, 2023 21:52
-
-
Save zanona/e077436e9a447f72fda2baf7fd123bc6 to your computer and use it in GitHub Desktop.
Update specific files on firebase hosting via a google cloud function
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
/** | |
* SOURCE: https://github.com/firebase/firebase-tools/tree/master/scripts/examples/hosting/update-single-file | |
* THIS IS AN EXPERIMENT OF THE ABOVE THAT COULD BE USED BY ANY CLOUD | |
* FUNCTION TO UPDATE HOSTING FILES (THINK OF A CMS). THE BENEFITS WOULD BE TO | |
* RUN THE CLOUD FUNCTION ONLY ONCE, WHILE HAVING THE HOSTING FILES UPDATED AND | |
* SERVED STATICALLY MUCH FASTER THROUGH A CDN. | |
* | |
* Usage: | |
* import {updateHostingFiles} from './lib/hosting'; | |
* await updateHostingFiles([{path: '/foo.json', data: '{version:2}']); | |
* console.log('success!') | |
*/ | |
import crypto from "node:crypto"; | |
import zlib from "node:zlib"; | |
import { Compute, GoogleAuth } from "google-auth-library"; | |
import { JSONClient } from "google-auth-library/build/src/auth/googleauth"; | |
const HOSTING_URL = "https://firebasehosting.googleapis.com/v1beta1"; | |
const PROJECT_ID = process.env.CLOUD_FIREBASE_PROJECT; | |
const SITE_ID = process.env.CLOUD_SITE_ID || PROJECT_ID; | |
type InputFiles = Array<{ path: FilePath; data: string }>; | |
type Client = Compute | JSONClient; | |
type VersionPath = `/sites/${string}/versions/${string}`; | |
type FilePath = `/${string}`; | |
type FileList = Array<{ path: FilePath; data: Buffer; hash: string }>; | |
function toFilePath(path: string) { | |
return path.replace(/^(\w)/, "/$1") as FilePath; | |
} | |
function hashFile(data: string) { | |
const hasher = crypto.createHash("sha256"); | |
const gzip = zlib.gzipSync(data, { level: 9 }); | |
return { hash: hasher.update(gzip).digest("hex"), data: gzip }; | |
} | |
async function getSiteVersion(siteId: string, client: Client) { | |
const site = await client.request<{ | |
url: string; | |
release: { name: string; version: { name: string } }; | |
}>({ | |
url: `${HOSTING_URL}/sites/${siteId}/channels/live`, | |
}); | |
//const release = site.data.release.name as `/sites/${string}/channels/live/releases/${string}`; | |
const currentVersion = site.data.release.version.name as VersionPath; | |
return currentVersion; | |
} | |
async function cloneSite( | |
siteId: string, | |
currentVersion: VersionPath, | |
skip: FileList, | |
client: Client | |
) { | |
const cloneOpPath = ( | |
await client.request<{ name: string }>({ | |
method: "POST", | |
url: `${HOSTING_URL}/sites/${siteId}/versions:clone`, | |
body: JSON.stringify({ | |
sourceVersion: currentVersion, | |
finalize: false, | |
exclude: { | |
regexes: skip.map((s) => s.path.replace("/", "\\/")), | |
}, | |
}), | |
}) | |
).data.name as `/projects/${string}/operations/${string}`; | |
let done = false; | |
let newVersion = ""; | |
while (!done) { | |
const opRes = await client.request<{ | |
done: boolean; | |
response: { name: string }; | |
}>({ | |
url: `${HOSTING_URL}/${cloneOpPath}`, | |
}); | |
done = !!opRes.data.done; | |
newVersion = opRes.data.response?.name as VersionPath; | |
console.debug("cloning site...done:", done); | |
await new Promise((resolve) => setTimeout(resolve, 1000)); | |
} | |
return newVersion as VersionPath; | |
} | |
async function uploadSiteFiles( | |
version: VersionPath, | |
files: FileList, | |
client: Client | |
) { | |
const fileList = files.reduce((p, c) => { | |
p[c.path] = c.hash; | |
return p; | |
}, {} as Record<FilePath, string>); | |
console.debug( | |
"preparing files", | |
files.map((f) => f.path) | |
); | |
const populateRes = await client.request<{ | |
uploadUrl: string; | |
uploadRequiredHashes?: string[]; | |
}>({ | |
method: "POST", | |
url: `${HOSTING_URL}/${version}:populateFiles`, | |
body: JSON.stringify({ files: fileList }), | |
}); | |
const uploadURL = populateRes.data.uploadUrl; | |
const uploadRequiredHashes = populateRes.data.uploadRequiredHashes || []; | |
if (Array.isArray(uploadRequiredHashes) && uploadRequiredHashes.length) { | |
for (const h of uploadRequiredHashes) { | |
const file = files.find((f) => f.hash === h); | |
console.debug("uploading %s...", file?.path); | |
const uploadRes = await client.request({ | |
method: "POST", | |
url: `${uploadURL}/${h}`, | |
data: file?.data, | |
}); | |
if (uploadRes.status !== 200) { | |
throw new Error(`Failed to upload file ${file?.path} (${h})`); | |
} | |
} | |
} | |
} | |
async function releaseSiteVersion( | |
siteId: string, | |
versionPath: VersionPath, | |
message: string, | |
client: Client | |
) { | |
console.debug("finalising site version"); | |
await client.request({ | |
method: "PATCH", | |
url: `${HOSTING_URL}/${versionPath}`, | |
params: { updateMask: "status" }, | |
body: JSON.stringify({ status: "FINALIZED" }), | |
}); | |
console.debug("releasing new site version"); | |
await client.request({ | |
method: "POST", | |
url: `${HOSTING_URL}/sites/${siteId}/releases`, | |
params: { versionName: versionPath }, | |
body: JSON.stringify({ message }), | |
}); | |
} | |
export async function updateHostingFiles(inputFiles: InputFiles) { | |
if (!PROJECT_ID) throw new Error("missing PROJECT_ID"); | |
if (!SITE_ID) throw new Error("missing SITE_ID"); | |
if (!Array.isArray(inputFiles)) | |
throw new Error("input needs to be an array"); | |
const auth = new GoogleAuth({ | |
scopes: "https://www.googleapis.com/auth/cloud-platform", | |
projectId: PROJECT_ID, | |
}); | |
const client = await auth.getClient(); | |
const files = inputFiles.map((f) => ({ | |
...hashFile(f.data), | |
path: toFilePath(f.path), | |
})); | |
const currentVersion = await getSiteVersion(SITE_ID, client); | |
const newVersionPath = await cloneSite( | |
SITE_ID, | |
currentVersion, | |
files, | |
client | |
); | |
await uploadSiteFiles(newVersionPath, files, client); | |
await releaseSiteVersion( | |
SITE_ID, | |
newVersionPath, | |
`update ${files.map((f) => f.path)}`, | |
client | |
); | |
return { ok: true }; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment