import type { SupabaseClient } from '@supabase/supabase-js' import { generateFullArchive, estimateArchiveSize, } from '@/lib/reports/full-archive-export' import { getOAuthEnv, refreshAccessToken, } from './google-oauth' import { ensureFolder, uploadFile } from './google-drive' import { decryptToken } from './crypto' import type { GoogleDriveConnection, GoogleDriveLastSync } from '../types' export const CONNECTION_KEY = 'google_drive_connection' export const LAST_SYNC_KEY = 'google_drive_last_sync' export const SCHEDULE_KEY = 'google_drive_schedule' export const ROOT_FOLDER_NAME = 'gnubok' export const SIZE_LIMIT_BYTES = 80 * 1024 * 1024 export type SyncFailureReason = | 'not_connected' | 'archive_too_large' | 'upload_failed' | 'internal' export type PerformSyncResult = | { ok: true lastSync: GoogleDriveLastSync webViewLink: string } | { ok: false reason: SyncFailureReason message: string size_bytes?: number size_limit_bytes?: number } interface PerformSyncParams { supabase: SupabaseClient companyId: string userId: string origin: string includeDocuments: boolean } /** * Run the end-to-end cloud backup sync: estimate size → refresh token → * ensure Drive folder hierarchy → generate archive → upload → persist * last_sync. Shared by the manual HTTP handler and the scheduled cron. * * Settings ops use raw `extension_data` queries (not the extension context * wrapper) because the cron runs under the service role with no user session. */ export async function performSync(params: PerformSyncParams): Promise { const { supabase, companyId, userId, origin, includeDocuments } = params const connection = await loadExtensionData( supabase, companyId, CONNECTION_KEY ) if (!connection) { return { ok: false, reason: 'not_connected', message: 'Google Drive not connected' } } const estimate = await estimateArchiveSize(supabase, companyId, 'all') const effectiveBytes = includeDocuments ? estimate.total_bytes : estimate.total_bytes - estimate.document_bytes if (effectiveBytes > SIZE_LIMIT_BYTES) { return { ok: false, reason: 'archive_too_large', message: 'Archive exceeds size limit', size_bytes: effectiveBytes, size_limit_bytes: SIZE_LIMIT_BYTES, } } const env = getOAuthEnv(origin) const refreshToken = decryptToken(connection.refresh_token_encrypted) const { access_token: accessToken } = await refreshAccessToken(env, refreshToken) let rootFolderId = connection.root_folder_id let companyFolderId = connection.company_folder_id if (!rootFolderId) { const root = await ensureFolder(accessToken, ROOT_FOLDER_NAME, null) rootFolderId = root.id } if (!companyFolderId) { const companyName = await fetchCompanyLabel(supabase, companyId) const companyFolder = await ensureFolder(accessToken, companyName, rootFolderId) companyFolderId = companyFolder.id } if ( rootFolderId !== connection.root_folder_id || companyFolderId !== connection.company_folder_id ) { await saveExtensionData(supabase, companyId, userId, CONNECTION_KEY, { ...connection, root_folder_id: rootFolderId, company_folder_id: companyFolderId, }) } const archive = await generateFullArchive(supabase, companyId, { scope: 'all', include_documents: includeDocuments, }) const stamp = new Date() .toISOString() .replace(/[-:]/g, '') .replace(/\..+/, '') const fileName = `arkiv_full_${stamp}.zip` const uploaded = await uploadFile(accessToken, companyFolderId, fileName, archive) const lastSync: GoogleDriveLastSync = { at: new Date().toISOString(), file_id: uploaded.id, file_name: uploaded.name, file_size_bytes: uploaded.size_bytes, folder_id: companyFolderId, } await saveExtensionData(supabase, companyId, userId, LAST_SYNC_KEY, lastSync) return { ok: true, lastSync, webViewLink: uploaded.web_view_link } } export async function loadExtensionData( supabase: SupabaseClient, companyId: string, key: string ): Promise { const { data } = await supabase .from('extension_data') .select('value') .eq('company_id', companyId) .eq('extension_id', 'cloud-backup') .eq('key', key) .maybeSingle() return (data?.value as T) ?? null } export async function saveExtensionData( supabase: SupabaseClient, companyId: string, userId: string, key: string, value: T ): Promise { const { error } = await supabase.from('extension_data').upsert( { user_id: userId, company_id: companyId, extension_id: 'cloud-backup', key, value, }, { onConflict: 'company_id,extension_id,key' } ) if (error) throw new Error(`Failed to save extension data: ${error.message}`) } async function fetchCompanyLabel( supabase: SupabaseClient, companyId: string ): Promise { const { data } = await supabase .from('company_settings') .select('company_name, org_number') .eq('company_id', companyId) .maybeSingle() const name = (data?.company_name as string) || 'företag' const org = (data?.org_number as string) || companyId.slice(0, 8) return `${name} (${org})`.replace(/[\\/]/g, '-') }