scripts/resize-images.ts
#!/usr/bin/env node -r esm -r ts-node/register
/*
To run:
-
- ensure accountKey has the correct service account data from firebase
- ensure projectId is set to co-reality-staging or co-reality-map
- set BACKUP to true to backup all images from firebase (in logical directory structure)
- set BACKUP to false to set resize all images to width COMPRESSION_WIDTH_PX and auto height
- tsconfig.json should contain
{
"compilerOptions": {
"target": "ES6",
"moduleResolution": "Node",
"traceResolution": false,
"allowJs": false,
"esModuleInterop": true,
"declaration": false,
"noResolve": false,
"noImplicitAny": false,
"removeComments": true,
"strictNullChecks": false,
"sourceMap": false,
"skipLibCheck": true,
"resolveJsonModule": true
}
}
once configured, in a terminal, run: npx ts-node resize-images.ts
*/
import fs from "fs";
import { resolve } from "path";
import admin from "firebase-admin";
import { GifUtil } from "gifwrap";
import jimp from "jimp";
import p from "phin";
import { uuid } from "uuidv4";
import { ACCEPTED_IMAGE_TYPES } from "../src/settings";
import { initFirebaseAdminApp, makeScriptUsage } from "./lib/helpers";
const usage = makeScriptUsage({
description: "Backup or resize images (see code comments for further usage)",
usageParams: "PROJECT_ID [CREDENTIAL_PATH]",
exampleParams: "co-reality-map [theMatchingAccountServiceKey.json]",
});
const [projectId, credentialPath] = process.argv.slice(2);
// Note: no need to check credentialPath here as initFirebaseAdmin defaults it when undefined
if (!projectId) {
usage();
}
// Max filesize permitted.
const MAX_SIZE_BYTES = 400 * 1024;
// If true, backup files. If false, run.
const BACKUP = false;
// Add entries here by hand to selectively process problem files.
// By default (with this list empty) script will process all files.
const SELECTIVELY_PROCESS_FILE_NAME_PARTS: string[] = [
// "BIGFILE.png",
// "HUGE_ANIMATED_GIF.gif",
];
initFirebaseAdminApp(projectId, {
credentialPath: credentialPath
? resolve(__dirname, credentialPath)
: undefined,
});
const backupFile = async (
remotePath: string,
signedUrl: string,
contentType: string
) => {
const backuplocation = `./backup/${remotePath}`;
if (fs.existsSync(backuplocation)) {
console.log("file exists", backuplocation);
return;
}
console.log("downloading", backuplocation);
try {
const backupDirectoryPath = backuplocation.replace(/[^/]+$/, "");
fs.mkdirSync(backupDirectoryPath, { recursive: true });
if (contentType === "image/gif" || remotePath.endsWith("gif")) {
const response = await p(signedUrl);
if ("headers" in response && "location" in response.headers) {
console.error(`Got redirect to ${response.headers.location}; skipping`);
return;
}
if (typeof response.body !== "object" || Buffer.isBuffer(response.body)) {
const msg =
"Could not load Buffer from <" +
signedUrl +
"> " +
"(HTTP: " +
response.statusCode +
")";
console.error(msg);
return;
}
const gifImage = await GifUtil.read(response.body);
return await GifUtil.write(backuplocation, gifImage.frames, gifImage);
} else {
// errors if the image is malformed
const jimpImage = await jimp.read(signedUrl);
return await jimpImage.writeAsync(backuplocation);
}
} catch (e) {
console.log(signedUrl);
console.log(e);
return;
}
};
const main = async () => {
const bucket = admin.storage().bucket();
console.log("Fetching files from bucket...");
const res = await bucket.getFiles({ directory: "users" });
const files = res[0];
console.log(`Fetched ${files.length} files.`);
for (const file of files) {
if (SELECTIVELY_PROCESS_FILE_NAME_PARTS.length > 0) {
let processFile = false;
SELECTIVELY_PROCESS_FILE_NAME_PARTS.forEach((filenamepiece) => {
if (file.name.includes(filenamepiece)) {
processFile = true;
}
});
const skipFile = !processFile;
if (skipFile) {
continue;
}
}
const [signedurl] = await file.getSignedUrl({
action: "read",
expires: "10-10-2020",
});
console.log("\n\n");
if (!ACCEPTED_IMAGE_TYPES.includes(file.metadata.contentType)) {
console.log(
`Skipping - ${file.metadata.contentType} Not a processible file type`
);
continue;
}
if (BACKUP) {
await backupFile(file.name, signedurl, file.metadata.contentType);
continue;
}
const resizeImage = async () => {
if (file.metadata.size <= MAX_SIZE_BYTES) {
console.log(
`Skipping ${file.name} - size is ${file.metadata.size}, <= ${MAX_SIZE_BYTES}`
);
return;
} else {
console.log(`${file.name} size is ${file.metadata.size} - resizing`);
}
const filename = file.name.replace(/^.*[\\/]/, "");
const resizedFilePath = `./temp/${filename}`;
if (
file.metadata.contentType === "image/gif" ||
file.name.endsWith("gif")
) {
const response = await p(signedurl);
if ("headers" in response && "location" in response.headers) {
console.error(
`Got redirect to ${response.headers.location}; skipping`
);
return;
}
if (
typeof response.body !== "object" ||
!Buffer.isBuffer(response.body)
) {
const msg =
"Could not load Buffer from <" +
signedurl +
"> " +
"(HTTP: " +
response.statusCode +
")";
console.error(msg);
return;
}
const gifImage = await GifUtil.read(response.body);
GifUtil.shareAsJimp(jimp, gifImage.frames[0]).resize(
Math.floor(gifImage.width / 2),
jimp.AUTO
);
await GifUtil.write(resizedFilePath, [gifImage.frames[0]], gifImage);
} else {
const jimpImage = await jimp.read(signedurl);
jimpImage.resize(Math.floor(jimpImage.getWidth() / 2), jimp.AUTO);
await jimpImage.writeAsync(resizedFilePath);
}
console.log(
`Wrote temp file for ${signedurl} to ${resizedFilePath}, size: ${
fs.statSync(resizedFilePath).size
}`
);
await bucket.upload(resizedFilePath, {
destination: file.name,
metadata: {
metadata: {
firebaseStorageDownloadTokens: uuid(),
},
},
});
console.log(`***** Overwrote ${file.name}`);
console.log(`Deleting ${resizedFilePath}`);
fs.unlinkSync(resizedFilePath);
};
try {
await resizeImage();
} catch (e) {
console.log("Error", e);
}
}
};
main();