How to download a csv file generated in the frontend

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. :innocent:

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