Skip to main content

Overview

QFieldCloud provides comprehensive file management capabilities with versioning, multiple storage backend support, and efficient file operations. The system handles both regular project files and packaged files for offline use.

Storage Architecture

Storage Backends

QFieldCloud supports multiple storage backends configured in Django’s STORAGES setting:
  • S3 - Amazon S3 storage
  • MinIO - Self-hosted S3-compatible storage
  • Custom - Any Django storage backend

File Storage Configuration

Projects can specify storage backends:
file_storage = models.CharField(
    max_length=100,
    validators=[validators.file_storage_name_validator],
    default=get_project_file_storage_default,
)

attachments_file_storage = models.CharField(
    max_length=100,
    validators=[validators.file_storage_name_validator],
    default=get_project_attachments_file_storage_default,
)
Separate storage for attachments allows optimization based on access patterns. See core/models.py:1262.

File Model

The File model represents a logical file within a project:
class File(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="all_files")
    name = models.CharField(max_length=255, validators=[filename_validator])
    file_type = models.PositiveSmallIntegerField(choices=FileType.choices)
    latest_version = models.ForeignKey("FileVersion", on_delete=models.DO_NOTHING)
    latest_version_count = models.PositiveIntegerField(default=0)
    uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    uploaded_at = models.DateTimeField(default=timezone.now)
See filestorage/models.py:41.

File Types

class FileType(models.IntegerChoices):
    PROJECT_FILE = 1, "Project File"
    PACKAGE_FILE = 2, "Package File"
  • PROJECT_FILE - Source files uploaded by users
  • PACKAGE_FILE - Generated files for mobile download

File Versioning

FileVersion Model

Each file upload creates a new version:
class FileVersion(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)
    file = models.ForeignKey(File, on_delete=models.CASCADE, related_name="versions")
    file_storage = models.CharField(max_length=100)
    content = DynamicStorageFileField(upload_to=get_file_version_upload_to)
    etag = models.CharField(max_length=64)
    md5sum = models.CharField(max_length=32)
    sha256sum = models.CharField(max_length=64)
    size = models.PositiveBigIntegerField()
    uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    uploaded_at = models.DateTimeField()
See filestorage/models.py:283.

Storage Path Structure

File versions are stored with the following pattern:

Regular Files

projects/{project_id}/files/{filename}/{display}-{version_id_short}
Example:
projects/abc-123/files/data.gpkg/v1-a1b2c3d4

Attachment Files (Versioned)

Same as regular files when are_attachments_versioned=True.

Attachment Files (Non-Versioned)

projects/{project_id}/files/{filename}
No version ID when are_attachments_versioned=False - only latest version kept.

Package Files

projects/{project_id}/packages/{package_job_id}/{filename}
See filestorage/models.py:262.

Version Retention

Control how many versions to keep:
storage_keep_versions = models.PositiveIntegerField(
    null=True,
    blank=True,
    validators=[MinValueValidator(1), MaxValueValidator(100)],
)
  • null - Use owner’s subscription plan default
  • 1-100 - Keep specified number of versions (Premium feature)
  • Older versions automatically purged
See core/models.py:1241.

File Operations

Uploading Files

API Endpoint

POST /api/v1/files/{projectid}/{filename}/

Request

Multipart form data with file content:
curl -X POST https://app.qfield.cloud/api/v1/files/{projectid}/data.gpkg/ \
  -H "Authorization: Token YOUR_API_TOKEN" \
  -F "file=@/path/to/data.gpkg"

Upload Process

  1. Validation
    • Check user permissions
    • Validate filename
    • Check storage quota
  2. Checksum Calculation
    md5sum, sha256sum = storage.calculate_checksums(content, ("md5", "sha256"))
    etag = calc_etag(content)
    
  3. Version Creation
    file_version = FileVersion.objects.add_version(
        project=project,
        filename=filename,
        content=content,
        file_type=File.FileType.PROJECT_FILE,
        uploaded_by=user,
    )
    
  4. Storage Backend Upload
    • Content uploaded to configured storage
    • Metadata (checksums) stored with version
  5. Post-Upload Actions
    • Update project’s data_last_updated_at
    • Trigger ProcessProjectfileJob for .qgs/.qgz files
    • Purge old versions if limit exceeded
See filestorage/models.py:158.

Downloading Files

List Files

GET /api/v1/files/{projectid}/
Response:
[
  {
    "name": "project.qgs",
    "size": 12345,
    "md5sum": "abc123...",
    "sha256": "def456...",
    "last_modified": "2024-01-15T10:30:00Z",
    "is_attachment": false,
    "versions": [
      {
        "version_id": "uuid-here",
        "size": 12345,
        "is_latest": true,
        "last_modified": "2024-01-15T10:30:00Z"
      }
    ]
  }
]

Download Specific Version

GET /api/v1/files/{projectid}/{filename}/?version={version_id}

Download Latest Version

GET /api/v1/files/{projectid}/{filename}/

Deleting Files

DELETE /api/v1/files/{projectid}/{filename}/
Deletes the file and all its versions from storage.

Attachment Files

Attachment Directory Configuration

Attachment directories are configured in project details:
@property
def attachment_dirs(self) -> list[str]:
    attachment_dirs = []
    
    if self.project_details and self.project_details.get("attachment_dirs"):
        attachment_dirs = self.project_details.get("attachment_dirs", [])
    
    if not attachment_dirs:
        attachment_dirs = ["DCIM"]
    
    return attachment_dirs
Default attachment directory is DCIM. See core/models.py:1535.

Attachment Versioning

Configure versioning behavior:
are_attachments_versioned = models.BooleanField(
    default=get_project_are_attachments_versioned_default,
    verbose_name="Versioned attachment files",
    help_text="If enabled, attachment files use versioning system. If disabled, only latest version kept"
)
Disabling versioning:
  • Saves storage space
  • Appropriate for photos and media
  • Filenames include extensions
See core/models.py:1319.

On-Demand Download

Reduce initial package size:
is_attachment_download_on_demand = models.BooleanField(
    default=False,
    verbose_name="On demand attachment files download",
    help_text="If enabled, attachment files downloaded on demand with QField"
)
When enabled:
  • Attachments excluded from initial package
  • QField fetches when viewing feature
  • Reduces mobile storage requirements
See core/models.py:1311.

Data Directories

Projects can define data directories for symbology and assets:
@property
def data_dirs(self) -> list[str]:
    data_dirs = []
    
    if self.project_details and self.project_details.get("data_dirs"):
        data_dirs = self.project_details.get("data_dirs", [])
    
    return data_dirs
Data directories:
  • Always included in packages
  • Hold SVG symbols, images, project plugins
  • Not treated as attachments
See core/models.py:1555.

Restricted Project Files

Limit who can modify QGIS project files:
has_restricted_projectfiles = models.BooleanField(
    default=False,
    verbose_name="Restrict project files",
    help_text="Restrict QGIS project configuration files to managers and administrators"
)
When enabled:
  • Only managers and admins can upload .qgs/.qgz/.qgd files
  • Prevents accidental project corruption
  • Other collaborators can still upload data files
See core/models.py:1206.

Storage Quota

Quota Calculation

@property
def storage_used_bytes(self) -> float:
    project_files_used_quota = (
        FileVersion.objects.filter(
            file__file_type=File.FileType.PROJECT_FILE,
            file__project__in=self.user.projects.exclude(
                file_storage=settings.LEGACY_STORAGE_NAME
            ),
        ).aggregate(sum_bytes=Sum("size"))["sum_bytes"]
        or 0
    )
    
    return project_files_used_quota
Quota includes:
  • All project file versions (based on retention policy)
  • Attachment files
  • Package files excluded from quota
See core/models.py:513.

Checking Available Quota

@property
def storage_free_bytes(self) -> float:
    return (
        self.current_subscription.active_storage_total_bytes
        - self.storage_used_bytes
    )
Uploads rejected when quota exceeded.

Checksums and Integrity

Checksum Types

Three checksums stored per version:
  1. MD5 - Quick file comparison
  2. SHA256 - Cryptographic verification
  3. ETag - S3-compatible identifier

Verification

Clients use checksums to:
  • Avoid re-uploading unchanged files
  • Verify download integrity
  • Detect file corruption

Metadata Storage

Checksums stored:
  • In database (FileVersion model)
  • In S3 object metadata (legacy storage)

File Permissions

Permissions checked before file operations:

Read Files

  • Project owner
  • All collaborators (any role)
  • Public project viewers

Upload Files

  • Admin, Manager, Editor roles
  • Managers/Admins only for restricted project files

Delete Files

  • Admin, Manager roles
  • Project owner

Legacy Storage Support

QFieldCloud maintains backward compatibility:
@property
def uses_legacy_storage(self) -> bool:
    return self.file_storage == settings.LEGACY_STORAGE_NAME
Legacy storage:
  • Uses S3 versioned buckets directly
  • Being phased out
  • Migration tools available
See core/models.py:1587.

Best Practices

File Organization

  1. Use consistent naming - Avoid special characters
  2. Organize in directories - Group related files
  3. Leverage attachment dirs - Keep media separate
  4. Configure data dirs - Include necessary assets

Performance

  1. Optimize file sizes - Compress where appropriate
  2. Use appropriate formats - GeoPackage for vector data
  3. Enable on-demand attachments - For large media collections
  4. Monitor storage quota - Regularly review file usage

Version Control

  1. Set appropriate retention - Balance between history and cost
  2. Version attachments selectively - Often not needed for photos
  3. Document major changes - Use commit messages where available
  4. Clean up old versions - Manually remove if needed

Security

  1. Use restricted project files - Protect critical configurations
  2. Review collaborator permissions - Grant appropriate file access
  3. Monitor file uploads - Check for unexpected changes
  4. Verify checksums - Ensure file integrity

API Reference

List All Files

GET /api/v1/files/{projectid}/
Query parameters:
  • skip_metadata=1 - Faster response without SHA256 checksums

Get File Metadata

GET /api/v1/files/{projectid}/{filename}/

Upload File

POST /api/v1/files/{projectid}/{filename}/
Content-Type: multipart/form-data

Download File

GET /api/v1/files/{projectid}/{filename}/
Query parameters:
  • version={version_id} - Download specific version

Delete File

DELETE /api/v1/files/{projectid}/{filename}/

Build docs developers (and LLMs) love