Skip to content

R2 Manual Versioning Implementation Plan

Context

Cloudflare R2 does not support native object versioning (unlike AWS S3). This is by design to keep R2 simple and cost-effective. However, for institutional requirements around file recovery and audit trails, we need a versioning strategy for user uploads (avatars, maintenance attachments).

Proposed Solution: Application-Level Versioning

Strategy Overview

Instead of relying on storage-level versioning, implement versioning at the application level using:

  • Timestamp-based naming for automatic versioning
  • Trash bucket approach for manual cleanup
  • Database tracking for version management

Implementation Approach

1. File Naming Convention

Current: avatars/{userId}/{filename}New: avatars/{userId}/{timestamp}_{filename}

Example:

avatars/123/1640995200_profile.jpg  (original upload)
avatars/123/1641081600_profile.jpg  (updated version)
avatars/123/1641168000_profile.jpg  (latest version)

2. Database Schema Enhancement

Add versioning tracking to avatar and attachment tables:

sql
-- Add to avatars table
ALTER TABLE avatars ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE avatars ADD COLUMN previous_versions TEXT[]; -- array of old keys

-- Add to maintenance_attachments table
ALTER TABLE maintenance_attachments ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE maintenance_attachments ADD COLUMN previous_versions TEXT[];

3. Storage Service Enhancement

Update StorageService class:

typescript
class StorageService {
  // Generate versioned key
  static avatarKey(userId: string, filename: string): string {
    const timestamp = Date.now();
    const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
    return `avatars/${userId}/${timestamp}_${safeName}`;
  }

  // Move to trash instead of delete
  async softDelete(key: string): Promise<void> {
    const trashKey = `trash/&#36;{Date.now()}_&#36;{key}`;
    await this.copy(key, trashKey);
    await this.delete(key);
  }
}

4. Trash Management Strategy

Option A: Dedicated Trash Bucket

  • Create separate R2 bucket: cepatedge-trash
  • Move deleted files there with retention period
  • Admin can review/approve permanent deletion

Option B: Prefixed Trash in Same Bucket

  • Keep in same bucket with trash/ prefix
  • Lifecycle rules can auto-delete after 30/90 days
  • Manual admin review process

5. Admin Interface

Add admin endpoints for trash management:

  • GET /admin/trash - List trash items
  • POST /admin/trash/:id/restore - Restore file
  • DELETE /admin/trash/:id - Permanent deletion

Timeline & Effort

Phase: Medium-priority task for Phase 4.5 completion or Phase 5 Effort: 4-6 hours implementation + testing Impact: High (addresses institutional file recovery requirements)

Benefits

  1. Institutional Compliance: Provides file recovery capabilities universities expect
  2. Cost Effective: No storage cost premium (unlike native versioning)
  3. Flexible: Can implement retention policies suitable for each institution
  4. Transparent: Clear audit trail of file changes

Migration Strategy

For existing files:

  1. Run migration script to add version=1 to existing records
  2. Keep existing files as-is (backward compatibility)
  3. New uploads use versioned naming

Success Criteria

  • ✅ Files can be "deleted" but recovered from trash
  • ✅ Version history visible to admins
  • ✅ Automatic cleanup after configurable retention period
  • ✅ No breaking changes to existing API
  • ✅ Clear audit trail for compliance

Alternative: Accept Current State

For pilot phase, document that:

  • Files are not versioned (R2 limitation)
  • Recovery requires database restore + manual recreation
  • Acceptable risk for initial pilot with small data volumes

Recommendation

Implement Option B (prefixed trash) as it's simpler and meets institutional requirements without additional infrastructure complexity.

This provides the file recovery capabilities universities expect while working within R2's design constraints.