When you upload an image called photo.jpg and there is already an existent photo.jpg, Directus preserves both files, each with a different filename_disk.
In my current project this default behaviour is not ideal and I would need to overwrite every image that is uploaded with the same filename_download, preserving as well the same filename_disk.
Sometimes we could have be 500 new images (to replace the old ones), and I cannot manually replace each one through the replace function.
I first tried to create a Flow, but flows don’t trigger on system collections like ‘directus_files’.
AI told me my best option is to create an extension hook, but none of the test I’ve done with AI worked.
So I’m basically clueless and appreciate any help.
2 Answers
2@jvmejias welcome to the forum!
My guess is that you have to use this as trigger of your flow:
If you are considering an extension hook, you could use my extension as an example:
Thanks a lot Attacler for putting me on the right track!
This is the final version of the extension, in case anyone is interested. Regenerating the base64 thumbnails from the new image was a bit tricky.
import fs from 'fs/promises';
import path from 'path';
import sharp from 'sharp';
export default ({ action }, { database, services, env, logger }) => {
const { AssetsService } = services;
action('files.upload', async (params, context) => {
const { payload, key: newKey } = params;
const filenameDownload = payload.filename_download;
if (!filenameDownload) return;
// Search for existing file with the same name
const existing = await database('directus_files')
.where('filename_download', filenameDownload)
.whereNot('id', newKey)
.first();
// Does not exist → normal upload
if (!existing) return;
const storageRoot =
env.STORAGE_LOCAL_ROOT || 'uploads';
const uploadsPath = path.join(
process.cwd(),
storageRoot
);
const oldPath = path.join(
uploadsPath,
existing.filename_disk
);
const newPath = path.join(
uploadsPath,
payload.filename_disk
);
// Global Directus cache
const rootCachePath = path.join(
process.cwd(),
'.cache'
);
try {
logger.info(
`[Overwrite] Replacing ${filenameDownload}`
);
// Read REAL metadata from the new file
const image = sharp(newPath);
const metadata = await image.metadata();
// Delete old physical file
try {
await fs.unlink(oldPath);
logger.info(
`[Overwrite] Old file deleted`
);
} catch (_) {}
// Clear global Directus cache
await fs.rm(rootCachePath, {
recursive: true,
force: true,
});
const now = new Date();
// Update ORIGINAL record
// keeping the SAME ID
// but using NEW filename_disk
await database('directus_files')
.where('id', existing.id)
.update({
filename_disk: payload.filename_disk,
filesize: payload.filesize,
type: payload.type,
width: metadata.width || null,
height: metadata.height || null,
modified_on: now,
uploaded_on: now,
});
// IMPORTANT:
// delete ONLY the temporary record
// DO NOT use FilesService.deleteOne()
// because it would also delete the binary
await database('directus_files')
.where('id', newKey)
.delete();
// Regenerate critical assets
const assetsService = new AssetsService({
schema: context.schema,
accountability: context.accountability,
});
const criticalTransforms = [
{ width: 64, height: 64, fit: 'contain', quality: 70 },
{ width: 120, height: 120, fit: 'contain', quality: 75 },
{ width: 300, fit: 'contain', quality: 80 },
{ width: 800, fit: 'contain', quality: 85 },
{ width: 1200, fit: 'contain', quality: 85 },
{},
];
for (const transform of criticalTransforms) {
try {
await assetsService.getAsset(
existing.id,
transform
);
} catch (_) {}
}
logger.info(
`[Overwrite] Completed for ID ${existing.id}`
);
} catch (err) {
logger.error('[Overwrite] Error:', err);
}
});
};
