Nicholai 017b0797c7
feat(files): Google Drive integration (#49)
* feat(files): wire file browser to Google Drive API

replace mock file data with real Google Drive integration
via domain-wide delegation (service account impersonation).

- add google drive REST API v3 library (JWT auth, drive
  client, rate limiting, token cache)
- add schema: google_auth, google_starred_files tables,
  users.googleEmail column
- add 16+ server actions for full CRUD (browse, upload,
  download, create folders, rename, move, trash/restore)
- add download proxy route with google-native file export
- add folder-by-ID route (/dashboard/files/folder/[id])
- refactor use-files hook to fetch from server actions
  with mock data fallback when disconnected
- update all file browser components (upload, rename,
  move, context menu, breadcrumb, drag-drop, nav)
- add settings UI: connection, shared drive picker,
  user email mapping
- extract shared AES-GCM crypto from netsuite module
- dual permission: compass RBAC + google workspace ACLs

* docs(files): add Google Drive integration guide

covers architecture decisions, permission model,
setup instructions, and known limitations.

---------

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-06 22:18:25 -07:00

136 lines
3.8 KiB
TypeScript
Executable File

// maps google drive API responses to our FileItem type
import type { FileItem, FileType, SharedUser, SharedRole } from "@/lib/files-data"
import type { DriveFile } from "./client/types"
const GOOGLE_APPS_FOLDER = "application/vnd.google-apps.folder"
const GOOGLE_APPS_DOCUMENT = "application/vnd.google-apps.document"
const GOOGLE_APPS_SPREADSHEET =
"application/vnd.google-apps.spreadsheet"
const GOOGLE_APPS_PRESENTATION =
"application/vnd.google-apps.presentation"
function mimeTypeToFileType(mimeType: string): FileType {
if (mimeType === GOOGLE_APPS_FOLDER) return "folder"
if (mimeType === GOOGLE_APPS_DOCUMENT) return "document"
if (mimeType === GOOGLE_APPS_SPREADSHEET)
return "spreadsheet"
if (mimeType === GOOGLE_APPS_PRESENTATION)
return "document"
if (mimeType === "application/pdf") return "pdf"
if (mimeType.startsWith("image/")) return "image"
if (mimeType.startsWith("video/")) return "video"
if (mimeType.startsWith("audio/")) return "audio"
if (
mimeType.includes("zip") ||
mimeType.includes("compressed") ||
mimeType.includes("archive") ||
mimeType.includes("tar") ||
mimeType.includes("gzip")
)
return "archive"
if (
mimeType.includes("spreadsheet") ||
mimeType.includes("excel") ||
mimeType === "text/csv"
)
return "spreadsheet"
if (
mimeType.includes("document") ||
mimeType.includes("word") ||
mimeType === "text/plain" ||
mimeType === "text/rtf"
)
return "document"
if (
mimeType.includes("javascript") ||
mimeType.includes("json") ||
mimeType.includes("xml") ||
mimeType.includes("html") ||
mimeType.includes("css") ||
mimeType.includes("typescript")
)
return "code"
return "unknown"
}
function mapPermissionRole(role: string): SharedRole {
if (role === "writer" || role === "owner") return "editor"
return "viewer"
}
export function mapDriveFileToFileItem(
driveFile: DriveFile,
starredIds: ReadonlySet<string>,
parentId: string | null = null
): FileItem {
const owner = driveFile.owners?.[0]
const sharedWith: SharedUser[] = (
driveFile.permissions ?? []
)
.filter(p => p.role !== "owner" && p.type === "user")
.map(p => ({
name: p.displayName ?? p.emailAddress ?? "Unknown",
avatar: p.photoLink,
role: mapPermissionRole(p.role),
}))
return {
id: driveFile.id,
name: driveFile.name,
type: mimeTypeToFileType(driveFile.mimeType),
mimeType: driveFile.mimeType,
size: driveFile.size ? Number(driveFile.size) : 0,
path: [],
createdAt: driveFile.createdTime ?? new Date().toISOString(),
modifiedAt:
driveFile.modifiedTime ?? new Date().toISOString(),
owner: {
name: owner?.displayName ?? "Unknown",
avatar: owner?.photoLink,
},
starred: starredIds.has(driveFile.id),
shared: sharedWith.length > 0 || driveFile.shared === true,
sharedWith:
sharedWith.length > 0 ? sharedWith : undefined,
trashed: driveFile.trashed ?? false,
parentId,
webViewLink: driveFile.webViewLink,
}
}
// export types for google-native files
export function isGoogleNativeFile(mimeType: string): boolean {
return mimeType.startsWith("application/vnd.google-apps.")
}
export function getExportMimeType(
googleMimeType: string
): string | null {
switch (googleMimeType) {
case GOOGLE_APPS_DOCUMENT:
return "application/pdf"
case GOOGLE_APPS_SPREADSHEET:
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
case GOOGLE_APPS_PRESENTATION:
return "application/pdf"
default:
return null
}
}
export function getExportExtension(
googleMimeType: string
): string {
switch (googleMimeType) {
case GOOGLE_APPS_DOCUMENT:
return ".pdf"
case GOOGLE_APPS_SPREADSHEET:
return ".xlsx"
case GOOGLE_APPS_PRESENTATION:
return ".pdf"
default:
return ""
}
}