Question:
How can I let the user download a dynamically generated csv file?
Product:
Wix Studio
What are you trying to achieve:
I have code that manages store credit (sa function not part of standard wix store). It works fine but I want to allow the admin user to download the audit records for a user. I am using Supabase to store the credit records and audit trail (because the WIX collections can’t do sql like sum(), or handle hundreds of thousands of records well.
I can see no way to trigger a download that would be a CSV of the current in memory dataset (displayed by a repeater).
What have you already tried:
I tried adding an A element (which is a common way to use a blob url (no access tot he document property).
I tried overwriting the html of a text element to insert the anchor witht the blob url(it rejects the blob url)
I tried uploading the data to the media collection (it uploaded but the was no way to then download it that I could see).
Additional information:
I’d personally upload to the media manager (I assuming you’re using this method - Upload | Velo)
In the return object, you get the fileUrl
, which you can return back to the frontend, and then use wixLocationFrontend.to()
to open the fileUrl
which will download to the users device - To | Velo
@noahlovell thanks. I started trying that method. I will probably go back and finish it. Did you figure out a way to get rid of these temp files?
Hi, @Mark_Overton !!
I’m not sure if it offers the level of usability you’re looking for, but there’s an API, moveFilesToTrash(), which is a sibling of the upload() API that Noah mentioned. So, it might not be impossible to use it to delete temporary files. 
this worked. I added code to check for old download each time it runs to clear them out.
1 Like
Here’s the code if anyone needs to do something similar:
import { mediaManager } from 'wix-media-backend';
import wixMembersBackend from 'wix-members-backend';
import { Permissions, webMethod } from "wix-web-module";
const requireManagerRole = async () => {
const roles = await wixMembersBackend.currentMember.getRoles();
if (!roles.some(r => r.title === "Store Credit Manager")) {
throw new Error("Unauthorized");
}
};
export async function getFolderIdByName(folderName) {
try {
const folders = await mediaManager.listFolders();
const targetFolder = folders.find(folder => folder.folderName === folderName);
if (!targetFolder) {
console.error(`Folder "${folderName}" not found.`);
throw new Error(`Folder "${folderName}" not found.`);
}
return targetFolder.folderId;
} catch (error) {
console.error('Error finding folder ID:', error);
throw error;
}
}
async function cleanupOldAuditFiles() {
try {
const folderId = await getFolderIdByName('auditdownloads');
const filters = {
parentFolderId: folderId
};
const files = await mediaManager.listFiles(filters, null, null);
const now = new Date();
const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000);
if (!files || !Array.isArray(files)) {
return;
}
for (const file of files) {
const filename = file.originalFileName || file.fileName;
const match = filename.match(/^audit_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})\.csv$/);
if (match) {
const timestampStr = match[1].replace(/T(\d{2})-(\d{2})-(\d{2})/, 'T$1:$2:$3') + 'Z';
const fileDate = new Date(timestampStr);
if (fileDate < fifteenMinutesAgo) {
const fileId = file.id || file.fileId || file._id || file.fileName;
if (!fileId) {
console.error('No valid file ID found, skipping deletion');
continue;
}
await mediaManager.moveFilesToTrash([fileId]);
}
}
}
} catch (error) {
console.warn('Error cleaning up old audit files:', error);
}
}
function escapeCsvField(field) {
if (field == null) return '';
const str = String(field);
if (str.includes(',') || str.includes('"') || str.includes('\\n') || str.includes('\\r')) {
return '"' + str.replace(/"/g, '""') + '"';
}
return str;
}
function generateCsvContent(auditData) {
const headers = [
'Date',
'Editor',
'Action',
'Original Amount',
'New Amount',
'Original Note',
'New Note',
'Audit Note'
];
const csvRows = [];
csvRows.push(headers.map(escapeCsvField).join(','));
auditData.forEach(item => {
const row = [
new Date(item.created_at).toLocaleString(),
item.editorUserDisplay || 'Unknown',
item.action || '',
(item.old_amount || 0).toFixed(2),
(item.new_amount || 0).toFixed(2),
item.old_note || '',
item.new_note || '',
item.audit_note || ''
];
csvRows.push(row.map(escapeCsvField).join(','));
});
return csvRows.join('\n');
}
export const uploadAuditCsv = webMethod(Permissions.SiteMember, async (auditData) => {
try {
await requireManagerRole();
if (!auditData || auditData.length === 0) {
throw new Error('No audit data provided');
}
await cleanupOldAuditFiles();
const csvContent = generateCsvContent(auditData);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const filename = `audit_${timestamp}.csv`;
const fileBuffer = Buffer.from(csvContent, 'utf8');
const uploadResult = await mediaManager.upload(
'/auditdownloads',
fileBuffer,
filename,
{
metadataOptions: {
isVisitorUpload: false
},
mediaOptions: {
mimeType: 'text/csv'
}
}
);
const fileId = uploadResult.fileId || uploadResult.id || uploadResult.fileName;
if (!fileId) {
console.error('No file ID found in upload result');
throw new Error('Upload succeeded but no file ID was returned');
}
const downloadUrl = await mediaManager.getDownloadUrl(fileId);
return {
success: true,
filename: filename,
downloadUrl: downloadUrl,
fileId: fileId
};
} catch (error) {
console.error('Failed to upload audit CSV:', error);
throw new Error(`Failed to upload audit CSV: ${error.message}`);
}
});
1 Like