functions/src/utils/cloudStorage.ts
import { tmpdir } from 'os'
import { join, dirname } from 'path'
import { unlinkSync, existsSync, promises as fs } from 'fs'
import mkdirp from 'mkdirp'
import * as admin from 'firebase-admin'
/**
* Read file and parse as json
* @param filePath - Path to file to read json from
*/
async function readJson(filePath: string) {
if (!existsSync(filePath)) {
throw new Error(`${filePath} does not exist`)
}
const fileBuffer = await fs.readFile(filePath)
try {
return JSON.parse(fileBuffer.toString())
} catch (err) {
console.error(`Error parsing JSON file ${filePath}`, err)
throw err
}
}
/**
* Read file and parse as json
* @param filePath - Path to file to read json from
*/
async function outputJson(filePath: string, fileContents: any) {
try {
await fs.writeFile(filePath, JSON.stringify(fileContents, null, 2))
} catch (err) {
console.error(`Error writing JSON file ${filePath}`, err)
throw err
}
}
/**
* Download JSON File from Google Cloud Storage and return is contents
* @param app - App from which the storage File should be downloaded
* @param pathInStorage - Path of file within cloud storage bucket
* @returns Resolves with JSON contents of the file
*/
export async function downloadFromStorage(
app: admin.app.App | null,
pathInStorage: string
): Promise<any> {
if (app && !app.storage) {
throw new Error('Storage is not enabled on firebase-admin')
}
// Handle default app
const bucket = !app ? admin.storage().bucket() : app.storage().bucket()
const localPath = `actions/storage/${pathInStorage}/${Date.now()}.json`
const tempLocalPath = join(tmpdir(), localPath)
const tempLocalDir = dirname(tempLocalPath)
try {
// Create Temporary directory and download file to that folder
await mkdirp(tempLocalDir)
// Download file from bucket to local filesystem
await bucket.file(pathInStorage).download({ destination: tempLocalPath })
} catch (err) {
const downloadErrMsg = 'Error downloading file from storage'
console.error(`${downloadErrMsg}: ${err.message || ''}`, err)
throw err
}
// TODO: Look into streaming results directly instead of needing to use a local file
try {
// Return JSON file contents
const fileContents = await readJson(tempLocalPath)
// Once the file data has been read, remove local files to free up disk space
unlinkSync(tempLocalPath)
return fileContents
} catch (err) {
const errMsg = 'Error saving file as JSON'
console.error(`${errMsg}: ${err.message || ''}`, err)
throw err
}
}
/**
* Upload JSON Object to Google Cloud Storage and return is contents
* @param app - App from which the storage File should be downloaded
* @param pathInStorage - Path of file within cloud storage bucket
* @param jsonObject - Object to upload to storage
* @returns Resolves with JSON contents of the file
*/
export async function uploadToStorage(
app: admin.app.App,
pathInStorage: string,
jsonObject: any
): Promise<void> {
const localPath = `actions/storage/${Date.now()}/${pathInStorage}.json`
const tempLocalPath = join(tmpdir(), localPath)
if (!app.storage) {
throw new Error('Storage is not enabled on firebase-admin')
}
try {
// Upload file from bucket to local filesystem
await outputJson(tempLocalPath, jsonObject)
// TODO: Look into using a buffer here instead of creating
await app.storage().bucket().upload(tempLocalPath, {
destination: pathInStorage,
contentType: 'application/json'
})
// Return JSON file contents
} catch (err) {
console.error('Error uploading file to storage', err.message || err)
throw err
}
}