Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
00f9e97
Add files architecture documentation
Sep 20, 2025
999747b
Add files test and code
Sep 20, 2025
7b624c4
lint
Sep 20, 2025
6d59ec4
Add file db support
Sep 20, 2025
477dd3d
Add files rout
Sep 20, 2025
1998cb8
Remove empty alembic revision
Sep 20, 2025
0e54889
Properly generate the files table
Sep 20, 2025
f1c67f4
Fix revision history
Sep 20, 2025
40311a6
Fix deprecation warning
Sep 21, 2025
41d8713
Lint
Sep 21, 2025
36de0e6
Implement file api tests
Sep 23, 2025
c632dbc
Lint
Sep 23, 2025
f9995a0
Add file registration scripts
Sep 23, 2025
c4d4c7b
Format with black
Sep 23, 2025
cd92088
Lint
Sep 23, 2025
f929f89
Update method/swagger doc
Sep 23, 2025
93c5b01
Merge branch 'bugfix_post_run' into files_api
Sep 23, 2025
17754f3
Fix sort_by field on runs
Sep 23, 2025
2737413
Fix sort_by on barcode to support asc/desc
Sep 23, 2025
73c4207
Lint
Sep 23, 2025
7d62f10
Ensure run_time is 4 digits when provided
Sep 23, 2025
c2c30a8
Ensure run_time is valid before adding to db and add test cases
Sep 23, 2025
ba55626
Add file browse endpoints
Sep 26, 2025
7e647d0
Fix test
Sep 27, 2025
708c59e
Format with black
Sep 27, 2025
0197383
Format with black
Sep 27, 2025
15ad92c
Add s3 support
Sep 27, 2025
a9bd8d5
Format with black
Sep 27, 2025
67882b1
fix Lint
Sep 27, 2025
d87eaee
Remove FileBrowserColumns model
Sep 27, 2025
564b394
Update exception handling
Sep 27, 2025
1a4b77e
Merge pull request #62 from NGS360/files_browse_api
golharam Sep 27, 2025
6a7d668
Implement dual-storage support for Illumina samplesheets
Sep 30, 2025
139ddfe
Black
Sep 30, 2025
9a25b90
Remove whitespace
Sep 30, 2025
6c12098
Merge pull request #63 from NGS360/files_browse_api
golharam Sep 30, 2025
395a314
Merge branch 'main' into files_api
Oct 1, 2025
637e826
Merge branch 'main' into files_api
Oct 1, 2025
47e1938
Merge in main
Oct 1, 2025
5a8bec2
Merge branch 'main' into files_api
Oct 1, 2025
6e5f361
Remove extraneous function
golharam Oct 2, 2025
3ed97c3
Merge in main
golharam Jan 8, 2026
581e260
Add S3 support to create_file
golharam Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
from core.config import get_settings

# Import all models here so that they are registered with SQLModel metadata
from api.samples.models import Sample, SampleAttribute
from api.files.models import File
from api.project.models import Project
from api.runs.models import SequencingRun
from api.samples.models import Sample, SampleAttribute
from api.vendors.models import Vendor
from api.workflow.models import Workflow, WorkflowAttribute

Expand All @@ -33,6 +34,7 @@
# target_metadata = None
target_metadata = SQLModel.metadata


# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
Expand Down
53 changes: 53 additions & 0 deletions alembic/versions/95817dee746c_add_files_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""add_files_table

Revision ID: 95817dee746c
Revises: c955364e5391
Create Date: 2025-09-20 14:28:45.987919

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision: str = '95817dee746c'
down_revision: Union[str, Sequence[str], None] = '43c1f122cf7f'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('file',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('file_id', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('filename', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
sa.Column('original_filename', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
sa.Column('file_path', sqlmodel.sql.sqltypes.AutoString(length=1024), nullable=False),
sa.Column('file_size', sa.Integer(), nullable=True),
sa.Column('mime_type', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True),
sa.Column('checksum', sqlmodel.sql.sqltypes.AutoString(length=64), nullable=True),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=1024), nullable=True),
sa.Column('file_type', sa.Enum('FASTQ', 'BAM', 'VCF', 'SAMPLESHEET', 'METRICS', 'REPORT', 'LOG', 'IMAGE', 'DOCUMENT', 'OTHER', name='filetype'), nullable=False),
sa.Column('upload_date', sa.DateTime(), nullable=False),
sa.Column('created_by', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=True),
sa.Column('entity_type', sa.Enum('PROJECT', 'RUN', name='entitytype'), nullable=False),
sa.Column('entity_id', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False),
sa.Column('storage_backend', sa.Enum('LOCAL', 'S3', 'AZURE', 'GCS', name='storagebackend'), nullable=False),
sa.Column('is_public', sa.Boolean(), nullable=False),
sa.Column('is_archived', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('file_id')
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('file')
# ### end Alembic commands ###
190 changes: 189 additions & 1 deletion api/files/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,195 @@
"""
Models for the Files API
"""
from sqlmodel import SQLModel

from typing import List
import uuid
from datetime import datetime
from enum import Enum

from sqlmodel import SQLModel, Field
from pydantic import ConfigDict


class FileType(str, Enum):
"""File type categories"""

FASTQ = "fastq"
BAM = "bam"
VCF = "vcf"
SAMPLESHEET = "samplesheet"
METRICS = "metrics"
REPORT = "report"
LOG = "log"
IMAGE = "image"
DOCUMENT = "document"
OTHER = "other"


class EntityType(str, Enum):
"""Entity types that can have files"""

PROJECT = "project"
RUN = "run"


class StorageBackend(str, Enum):
"""Storage backend types"""

LOCAL = "local"
S3 = "s3"
AZURE = "azure"
GCS = "gcs"


class File(SQLModel, table=True):
"""Core file entity that can be associated with runs or projects"""

__searchable__ = ["filename", "description", "file_id"]

id: uuid.UUID | None = Field(default_factory=uuid.uuid4, primary_key=True)
file_id: str = Field(unique=True, max_length=100) # Human-readable identifier
filename: str = Field(max_length=255)
original_filename: str = Field(max_length=255)
file_path: str = Field(max_length=1024) # Storage path/URI
file_size: int | None = None # Size in bytes
mime_type: str | None = Field(default=None, max_length=100)
checksum: str | None = Field(default=None, max_length=64) # SHA-256 hash

# Metadata
description: str | None = Field(default=None, max_length=1024)
file_type: FileType = Field(default=FileType.OTHER)
upload_date: datetime = Field(default_factory=datetime.utcnow)
created_by: str | None = Field(default=None, max_length=100) # User identifier

# Polymorphic associations
entity_type: EntityType # "project" or "run"
entity_id: str = Field(max_length=100) # project_id or run barcode

# Storage metadata
storage_backend: StorageBackend = Field(default=StorageBackend.LOCAL)
is_public: bool = Field(default=False)
is_archived: bool = Field(default=False)

model_config = ConfigDict(from_attributes=True)

def generate_file_id(self) -> str:
"""Generate a unique file ID"""
import secrets
import string

alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(12))


class FileCreate(SQLModel):
"""Request model for creating a file"""

filename: str
original_filename: str | None = None
description: str | None = None
file_type: FileType = FileType.OTHER
entity_type: EntityType
entity_id: str
is_public: bool = False
created_by: str | None = None

model_config = ConfigDict(extra="forbid")


class FileUpdate(SQLModel):
"""Request model for updating file metadata"""

filename: str | None = None
description: str | None = None
file_type: FileType | None = None
is_public: bool | None = None
is_archived: bool | None = None

model_config = ConfigDict(extra="forbid")


class FilePublic(SQLModel):
"""Public file representation"""

file_id: str
filename: str
original_filename: str
file_size: int | None
mime_type: str | None
description: str | None
file_type: FileType
upload_date: datetime
created_by: str | None
entity_type: EntityType
entity_id: str
is_public: bool
is_archived: bool
storage_backend: StorageBackend
checksum: str | None = None


class FilesPublic(SQLModel):
"""Paginated file listing"""

data: List[FilePublic]
total_items: int
total_pages: int
current_page: int
per_page: int
has_next: bool
has_prev: bool


class FileUploadRequest(SQLModel):
"""Request model for file upload"""

filename: str
description: str | None = None
file_type: FileType = FileType.OTHER
is_public: bool = False

model_config = ConfigDict(extra="forbid")


class FileUploadResponse(SQLModel):
"""Response model for file upload"""

file_id: str
filename: str
file_size: int | None = None
checksum: str | None = None
upload_date: datetime
message: str = "File uploaded successfully"


class FileFilters(SQLModel):
"""File filtering options"""

entity_type: EntityType | None = None
entity_id: str | None = None
file_type: FileType | None = None
mime_type: str | None = None
created_by: str | None = None
is_public: bool | None = None
is_archived: bool | None = None
search_query: str | None = None # Search in filename/description

model_config = ConfigDict(extra="forbid")


class PaginatedFileResponse(SQLModel):
"""Paginated response for file listings"""

data: list[FilePublic]
total_items: int
total_pages: int
current_page: int
per_page: int
has_next: bool
has_prev: bool

model_config = ConfigDict(from_attributes=True)


class FileBrowserFolder(SQLModel):
Expand Down
Loading
Loading