package migration import ( "context" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) const ( SharedDriveModeAuto = "auto" SharedDriveModeManual = "manual" SharedDriveStatusPending = "pending" SharedDriveStatusApproved = "approved" SharedDriveStatusRejected = "rejected" ) type SharedDrive struct { ID string `json:"id"` ProjectID string `json:"project_id"` DriveID string `json:"drive_id"` Name string `json:"name"` Status string `json:"status"` DiscoveredByUserID *string `json:"discovered_by_user_id,omitempty"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } func NormalizeSharedDriveMode(mode string) string { switch mode { case SharedDriveModeManual: return SharedDriveModeManual default: return SharedDriveModeAuto } } // SharedDriveItemStore tracks project-level imports for shared drive files (cross-user dedup). type SharedDriveItemStore struct { db *pgxpool.Pool projectID string done map[string]struct{} // key: driveID + ":" + sourceID } func NewSharedDriveItemStoreMemory() *SharedDriveItemStore { return &SharedDriveItemStore{done: map[string]struct{}{}} } func LoadSharedDriveItemStore(ctx context.Context, db *pgxpool.Pool, projectID string) (*SharedDriveItemStore, error) { store := &SharedDriveItemStore{ db: db, projectID: projectID, done: map[string]struct{}{}, } if db == nil || projectID == "" { return store, nil } rows, err := db.Query(ctx, ` SELECT drive_id, source_id FROM migration_shared_drive_items WHERE project_id = $1::uuid `, projectID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { var driveID, sourceID string if err := rows.Scan(&driveID, &sourceID); err != nil { return nil, err } store.done[sharedDriveItemKey(driveID, sourceID)] = struct{}{} } return store, rows.Err() } func sharedDriveItemKey(driveID, sourceID string) string { return driveID + ":" + sourceID } func (s *SharedDriveItemStore) Has(driveID, sourceID string) bool { if s == nil || driveID == "" || sourceID == "" { return false } _, ok := s.done[sharedDriveItemKey(driveID, sourceID)] return ok } func (s *SharedDriveItemStore) MarkImported(ctx context.Context, driveID, sourceID, relPath, jobID string) error { if driveID == "" || sourceID == "" { return nil } s.done[sharedDriveItemKey(driveID, sourceID)] = struct{}{} if s.db == nil || s.projectID == "" { return nil } _, err := s.db.Exec(ctx, ` INSERT INTO migration_shared_drive_items (project_id, drive_id, source_id, rel_path, imported_by_job_id) VALUES ($1::uuid, $2, $3, $4, NULLIF($5, '')::uuid) ON CONFLICT (project_id, drive_id, source_id) DO NOTHING `, s.projectID, driveID, sourceID, relPath, jobID) return err } func (s *Service) UpdateSharedDriveMode(ctx context.Context, projectID, mode string) (Project, error) { mode = NormalizeSharedDriveMode(mode) sc := newProjectScanner() err := s.db.QueryRow(ctx, ` UPDATE migration_projects SET shared_drive_mode = $2, updated_at = NOW() WHERE id = $1::uuid RETURNING `+projectSelectSQL("")+` `, projectID, mode).Scan(sc.targets()...) return sc.result(), err } func (s *Service) ListSharedDrives(ctx context.Context, projectID, statusFilter string) ([]SharedDrive, error) { query := ` SELECT id::text, project_id::text, drive_id, name, status, NULLIF(discovered_by_user_id::text, ''), created_at::text, updated_at::text FROM migration_shared_drives WHERE project_id = $1::uuid ` args := []any{projectID} if statusFilter != "" { query += ` AND status = $2` args = append(args, statusFilter) } query += ` ORDER BY name ASC, created_at ASC` rows, err := s.db.Query(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var out []SharedDrive for rows.Next() { var row SharedDrive if err := rows.Scan( &row.ID, &row.ProjectID, &row.DriveID, &row.Name, &row.Status, &row.DiscoveredByUserID, &row.CreatedAt, &row.UpdatedAt, ); err != nil { return nil, err } out = append(out, row) } return out, rows.Err() } func (s *Service) SetSharedDriveStatus(ctx context.Context, projectID, driveID, status string) (SharedDrive, error) { var row SharedDrive err := s.db.QueryRow(ctx, ` UPDATE migration_shared_drives SET status = $3, updated_at = NOW() WHERE project_id = $1::uuid AND drive_id = $2 RETURNING id::text, project_id::text, drive_id, name, status, NULLIF(discovered_by_user_id::text, ''), created_at::text, updated_at::text `, projectID, driveID, status).Scan( &row.ID, &row.ProjectID, &row.DriveID, &row.Name, &row.Status, &row.DiscoveredByUserID, &row.CreatedAt, &row.UpdatedAt, ) if err != nil { return SharedDrive{}, fmt.Errorf("shared drive not found") } return row, nil } func (s *Service) ApproveSharedDrive(ctx context.Context, projectID, driveID string) (SharedDrive, error) { return s.SetSharedDriveStatus(ctx, projectID, driveID, SharedDriveStatusApproved) } func (s *Service) RejectSharedDrive(ctx context.Context, projectID, driveID string) (SharedDrive, error) { return s.SetSharedDriveStatus(ctx, projectID, driveID, SharedDriveStatusRejected) } func (d *DriveImporter) upsertDiscoveredSharedDrive(ctx context.Context, projectID, userID, driveID, name, mode string) error { if d.db == nil { return nil } autoApprove := NormalizeSharedDriveMode(mode) == SharedDriveModeAuto initialStatus := SharedDriveStatusPending if autoApprove { initialStatus = SharedDriveStatusApproved } _, err := d.db.Exec(ctx, ` INSERT INTO migration_shared_drives (project_id, drive_id, name, status, discovered_by_user_id) VALUES ($1::uuid, $2, $3, $4, NULLIF($5, '')::uuid) ON CONFLICT (project_id, drive_id) DO UPDATE SET name = COALESCE(NULLIF(EXCLUDED.name, ''), migration_shared_drives.name), status = CASE WHEN migration_shared_drives.status = 'rejected' THEN 'rejected' WHEN migration_shared_drives.status = 'approved' THEN 'approved' WHEN $6 = 'auto' THEN 'approved' ELSE migration_shared_drives.status END, updated_at = NOW() `, projectID, driveID, name, initialStatus, userID, NormalizeSharedDriveMode(mode)) return err } func (d *DriveImporter) loadApprovedSharedDriveFolders(ctx context.Context, projectID string) ([]driveFolderRef, error) { if d.db == nil { return nil, nil } rows, err := d.db.Query(ctx, ` SELECT drive_id, name FROM migration_shared_drives WHERE project_id = $1::uuid AND status = 'approved' ORDER BY name ASC `, projectID) if err != nil { return nil, err } defer rows.Close() var out []driveFolderRef for rows.Next() { var id, name string if err := rows.Scan(&id, &name); err != nil { return nil, err } out = append(out, driveFolderRef{ ID: id, Path: pathJoinSharedDrive(name), DriveID: id, Shared: true, }) } return out, rows.Err() } func pathJoinSharedDrive(name string) string { return "Shared Drives/" + sanitizeDrivePath(name) }