Docs / architecture/google-drive-file-storage-apps-script.md

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:

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:

Standard reserved folders:

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.