Skip to content

Instantly share code, notes, and snippets.

@castortech
Created October 14, 2024 13:31
Show Gist options
  • Save castortech/67d5eca409267296d7a6cdf242e382d1 to your computer and use it in GitHub Desktop.
Save castortech/67d5eca409267296d7a6cdf242e382d1 to your computer and use it in GitHub Desktop.
Modified version to restore extended attributes on Supabase storage.
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();
{
"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