Chimti Google Drive File Storage Apps Script
Chimti backend stores business records in the database, but operational uploaded files should live in Google Drive. The backend talks to a private Google Apps Script web app and stores only Drive metadata in the database.
Required Environment
Set these on the Chimti API service:
FILE_STORAGE_PROVIDER=google_drive_apps_script
FILE_STORAGE_APPS_SCRIPT_URL=https://script.google.com/macros/s/.../exec
FILE_STORAGE_APPS_SCRIPT_SECRET=<long random shared secret>
FILE_STORAGE_ROOT_FOLDER=Chimti Files
PUBLIC_API_BASE_URL=https://api.chimti.ai
Do not commit the Apps Script URL secret or Google credentials.
Apps Script Web App Contract
Deploy the script as a web app with access limited to the script owner/service account context that owns the Drive folders. The backend sends JSON POST requests.
Supported actions:
- `uploadFile`: receives `folderPath`, `fileName`, `mimeType`, `base64Data`, `metadata`; returns Drive `fileId`, `webViewLink`, `fileName`, `mimeType`, and `sizeBytes`.
- `downloadFile`: receives `fileId`; returns `base64Data`, `fileName`, and `mimeType`.
- `deleteFile`: receives `fileId`; trashes/removes the Drive file.
- `listFolderTree`: receives `folderPath` or `folderId`, `depth`, `includeFiles`, and `maxItems`; returns a read-only nested folder/file tree for the Chimti Admin Drive Files module.
Reference Apps Script
const SHARED_SECRET = PropertiesService.getScriptProperties().getProperty('CHIMTI_FILE_STORAGE_SECRET');
function doPost(e) {
try {
const body = JSON.parse(e.postData.contents || '{}');
if (!SHARED_SECRET || body.secret !== SHARED_SECRET) {
return json({ ok: false, error: 'Unauthorized' }, 401);
}
if (body.action === 'uploadFile') return uploadFile(body);
if (body.action === 'downloadFile') return downloadFile(body);
if (body.action === 'deleteFile') return deleteFile(body);
if (body.action === 'listFolderTree') return listFolderTree(body);
return json({ ok: false, error: 'Unsupported action' }, 400);
} catch (error) {
return json({ ok: false, error: error.message || 'Apps Script failure' }, 500);
}
}
function uploadFile(body) {
const folder = ensureFolderPath(body.folderPath || 'Chimti Files');
const bytes = Utilities.base64Decode(body.base64Data || '');
const blob = Utilities.newBlob(bytes, body.mimeType || 'application/octet-stream', body.fileName || 'chimti-file');
const file = folder.createFile(blob);
file.setDescription(JSON.stringify(body.metadata || {}));
return json({
ok: true,
file: {
fileId: file.getId(),
fileName: file.getName(),
mimeType: file.getMimeType(),
sizeBytes: file.getSize(),
webViewLink: file.getUrl()
}
});
}
function downloadFile(body) {
const file = DriveApp.getFileById(body.fileId);
return json({
ok: true,
file: {
fileId: file.getId(),
fileName: file.getName(),
mimeType: file.getMimeType(),
sizeBytes: file.getSize(),
base64Data: Utilities.base64Encode(file.getBlob().getBytes())
}
});
}
function deleteFile(body) {
DriveApp.getFileById(body.fileId).setTrashed(true);
return json({ ok: true });
}
function listFolderTree(body) {
const folder = body.folderId
? DriveApp.getFolderById(body.folderId)
: ensureFolderPath(body.folderPath || 'Chimti Files');
const state = {
count: 0,
maxItems: Math.min(Math.max(Number(body.maxItems || 1000), 1), 5000),
};
const tree = serializeFolder(folder, 0, Math.min(Math.max(Number(body.depth || 6), 0), 10), body.includeFiles !== false, state);
return json({
ok: true,
tree,
totalItems: state.count,
truncated: state.count >= state.maxItems
});
}
function serializeFolder(folder, depth, maxDepth, includeFiles, state) {
state.count += 1;
const node = {
id: folder.getId(),
name: folder.getName(),
type: 'folder',
createdAt: folder.getDateCreated().toISOString(),
updatedAt: folder.getLastUpdated().toISOString(),
webViewLink: folder.getUrl(),
children: []
};
if (depth >= maxDepth || state.count >= state.maxItems) {
return node;
}
const folders = folder.getFolders();
while (folders.hasNext() && state.count < state.maxItems) {
node.children.push(serializeFolder(folders.next(), depth + 1, maxDepth, includeFiles, state));
}
if (includeFiles) {
const files = folder.getFiles();
while (files.hasNext() && state.count < state.maxItems) {
const file = files.next();
state.count += 1;
node.children.push({
id: file.getId(),
name: file.getName(),
type: 'file',
mimeType: file.getMimeType(),
sizeBytes: file.getSize(),
createdAt: file.getDateCreated().toISOString(),
updatedAt: file.getLastUpdated().toISOString(),
webViewLink: file.getUrl()
});
}
}
return node;
}
function ensureFolderPath(path) {
const parts = String(path || 'Chimti Files').split('/').map((part) => part.trim()).filter(Boolean);
let current = DriveApp.getRootFolder();
parts.forEach((part) => {
const existing = current.getFoldersByName(part);
current = existing.hasNext() ? existing.next() : current.createFolder(part);
});
return current;
}
function json(payload) {
return ContentService
.createTextOutput(JSON.stringify(payload))
.setMimeType(ContentService.MimeType.JSON);
}
Current Backend Use
The storage layer is used for new operational uploads across the Chimti apps:
- Order item photos: `Chimti Files/brands/{brandId}/stores/{storeId}/orders/{orderCode}/items/{itemCode}/photos`.
- Staff documents: `Chimti Files/brands/{brandId}/stores/{storeId}/staff/{staffId}/documents`.
- User profile avatars: `Chimti Files/platform/users/{userId}/avatars`.
- Brand, store, and platform logos: `Chimti Files/platform/workspace-settings/{scopeKey}/logos`.
Standard reserved folders:
- `Chimti Files/platform/leads/{leadId}/attachments` for future lead attachments.
- `Chimti Files/platform/clients/{clientId}/contracts` for client contracts and signed onboarding documents.
- `Chimti Files/platform/clients/{clientId}/kyc` for client GST, legal, business verification, and KYC documents.
- `Chimti Files/platform/clients/{clientId}/onboarding` for setup sheets, rollout assets, and handoff files.
- `Chimti Files/platform/reports/generated` for generated admin reports.
- `Chimti Files/platform/ai/generated-reports` for AI-generated PDF reports, owner briefs, and business decision files.
- `Chimti Files/platform/ai/summaries` for AI summaries, recommendations, and generated text artifacts.
- `Chimti Files/platform/print/templates` for print, invoice, receipt, and tag templates.
- `Chimti Files/platform/print/tag-files` for generated print/tag/label files.
- `Chimti Files/brands/{brandId}/stores/{storeId}/customers/{customerId}/attachments` for future customer documents.
- `Chimti Files/brands/{brandId}/stores/{storeId}/communications/whatsapp-media` for future WhatsApp media.
- `Chimti Files/brands/{brandId}/stores/{storeId}/communications/email-attachments` for future email attachments.
- `Chimti Files/brands/{brandId}/stores/{storeId}/calls/recordings` for call recordings and voice-agent call audio.
- `Chimti Files/brands/{brandId}/stores/{storeId}/calls/voice-notes` for short voice notes or audio evidence.
- `Chimti Files/brands/{brandId}/stores/{storeId}/payments/reconciliation` for payment gateway settlement/reconciliation files.
- `Chimti Files/brands/{brandId}/stores/{storeId}/invoices/pdfs` for generated invoice PDFs.
- `Chimti Files/brands/{brandId}/stores/{storeId}/invoices/receipts` for receipt PDFs and payment acknowledgements.
- `Chimti Files/brands/{brandId}/stores/{storeId}/imports` and `exports` for data movement.
- `Chimti Files/public/website-leads/attachments` for public lead attachments if added later.
- `Chimti Files/temporary/uploads` for short-lived upload handoff files before final entity mapping.
For these records, the database stores metadata, Drive file ids, and audit context. File reads go through signed/proxied API URLs. Legacy database `imageDataUrl` records remain readable for old files, but new uploads should not create long-lived base64 blobs in the database.