Created
October 14, 2024 13:31
-
-
Save castortech/67d5eca409267296d7a6cdf242e382d1 to your computer and use it in GitHub Desktop.
Modified version to restore extended attributes on Supabase storage.
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 'dotenv/config' | |
import { SupabaseClient, createClient } from '@supabase/supabase-js' | |
import * as fs from 'fs/promises' | |
import * as path from 'path' | |
import { exec } from 'child_process' | |
import { promisify } from 'util' | |
const execAsync = promisify(exec); | |
async function setFileMetadata(filePath: string, key: string, value: string): Promise<void> { | |
try { | |
// Convert the key and value to a format suitable for xattr | |
const formattedKey = `user.${key}`; | |
// Write metadata using xattr | |
await execAsync(`xattr -w ${formattedKey} '${value.replace(`'`, `\'`)}' '${filePath.replace(`'`, `\'`)}'`); | |
} catch (error) { | |
console.error(`Error setting metadata: ${error}`); | |
} | |
} | |
let processed = 0 | |
let skipped = 0 | |
async function searchFiles( | |
directory: string, | |
callback: ( | |
fileName: string, | |
fullPath: string | |
) => Promise<void>, | |
): Promise<void> { | |
async function traverse(currentDirectory: string): Promise<void> { | |
const files = await fs.readdir(currentDirectory); | |
for (const file of files) { | |
const filePath = path.join(currentDirectory, file); | |
const stat = await fs.stat(filePath); | |
if (stat.isDirectory()) { | |
// If it's a directory, recursively execute the function | |
await traverse(filePath); | |
} else { | |
// If it's a file, execute the callback function | |
await callback(file, filePath); | |
} | |
} | |
} | |
// Start the traversal | |
await traverse(directory); | |
} | |
async function searchCallback( | |
client: SupabaseClient, | |
file: string, | |
fullPath: string | |
): Promise<void> { | |
// Skip the MacOS `.DS_Store` | |
if (file === ".DS_Store") { | |
return; | |
} | |
const paths = fullPath.split("/"); | |
const containingFolder = paths.slice(5, -1).join("/") | |
const bucket = paths[4] | |
const storageObjectData = await getFileObjectDataFromDb(client, file, containingFolder, bucket) | |
if (storageObjectData == undefined) { | |
console.debug('skipped') | |
skipped++ | |
return | |
} | |
if (storageObjectData.version !== file) { | |
console.debug('Invalid version, found:' + storageObjectData.version + ', expected:' + file + ' - path: ' + paths) | |
console.error('ERROR') | |
skipped++ | |
return | |
// process.exit(1) | |
} | |
// console.debug('processed: ' + fullPath) | |
await setFileMetadata(fullPath, 'supabase.cache-control', storageObjectData.metadata.cacheControl) | |
await setFileMetadata(fullPath, 'supabase.content-type', storageObjectData.metadata.mimetype) | |
processed++ | |
} | |
/** Get file Object data from the database (storage.objects) */ | |
const getFileObjectDataFromDb = async ( | |
client: SupabaseClient, | |
fileName: string, | |
folderName: string, | |
bucketName: string | |
): Promise<{ version: string; metadata: { eTag: string; size: number; mimetype: string; cacheControl: string; | |
lastModified: string; contentLength: number; httpStatusCode: number }} | undefined> => { | |
const result = await client | |
.schema('storage') | |
.from('objects') | |
.select('*') | |
.eq('bucket_id', bucketName) | |
.eq('name', folderName) | |
.single() | |
if (result.error) { | |
console.debug('file name:' + fileName + ' - folder name:' + folderName + ' - bucket name: ' + bucketName) | |
console.error(result.error) | |
return undefined | |
} | |
return result.data | |
} | |
const mainLoop = async () => { | |
console.log('start') | |
// Initialize the Supabase client using the configurations placed inside the `.env` file | |
const client = createClient(process.env.SUPABASE_URL || '', process.env.SERVICE_ROLE_KEY || '') | |
await searchFiles( | |
'volumes/storage/stub/stub', | |
async ( | |
file: string, | |
fullPath: string | |
) => { | |
return searchCallback(client, file, fullPath) | |
} | |
) | |
console.log('\n\n') | |
console.log('processed: ' + processed) | |
console.log('skipped: ' + skipped) | |
console.log('end') | |
} | |
mainLoop(); |
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
Show hidden characters
{ | |
"compilerOptions": { | |
"target": "ES2020", | |
"module": "CommonJS", | |
"strict": true, | |
"esModuleInterop": true, | |
"moduleResolution": "node", | |
"skipLibCheck": true, | |
"forceConsistentCasingInFileNames": true | |
}, | |
"include": ["**/*.ts"], | |
"exclude": ["node_modules"] | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment