Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
__pycache__
.git
.env
alembic/versions
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,9 @@ docker-compose.override.yml
# ======================
.DS_Store
Thumbs.db
test_data/
test_data/

# ======================
#Runtime files
# ======================
temp/
1 change: 1 addition & 0 deletions alembic/versions/ecca7b879961_genesis_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

op.execute("CREATE EXTENSION IF NOT EXISTS vector")

def upgrade() -> None:
"""Upgrade schema."""
Expand Down
39 changes: 39 additions & 0 deletions alembic/versions/efb17bbdead1_add_watched_folders_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""add watched folders table

Revision ID: efb17bbdead1
Revises: ecca7b879961
Create Date: 2026-07-02 12:29:07.453901

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
import pgvector.sqlalchemy


# revision identifiers, used by Alembic.
revision: str = 'efb17bbdead1'
down_revision: Union[str, Sequence[str], None] = 'ecca7b879961'
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('watched_folders',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('folder_path', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('folder_path')
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('watched_folders')
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from app.routes import search
from app.routes import upload
from app.routes import files
from app.routes import watch

app = FastAPI(
title="WhereTF Backend"
Expand All @@ -10,6 +11,7 @@
app.include_router(search.router)
app.include_router(upload.router)
app.include_router(files.router)
app.include_router(watch.router)

@app.get("/health", tags=["System"])
def health_check():
Expand Down
3 changes: 2 additions & 1 deletion app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from app.models.file import File
from app.models.content import FileContent
from app.models.relationship import FileRelationship
from app.models.relationship import FileRelationship
from app.models.watched_folder import WatchedFolder
27 changes: 27 additions & 0 deletions app/models/watched_folder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from datetime import datetime

from sqlalchemy import DateTime, Text
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func

from app.database import Base


class WatchedFolder(Base):
__tablename__ = "watched_folders"

id: Mapped[int] = mapped_column(
primary_key=True,
autoincrement=True
)

folder_path: Mapped[str] = mapped_column(
Text,
unique=True,
nullable=False
)

created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now()
)
74 changes: 73 additions & 1 deletion app/routes/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,76 @@ def delete_file(file_id: uuid.UUID, db: Session = Depends(get_db)):
db.delete(db_file)
db.commit()

return {"status": "success", "message": f"File and vectors wiped successfully."}
return {"status": "success", "message": f"File and vectors wiped successfully."}


class FilePathRequest(BaseModel):
file_path: str


@router.post("/delete-by-path")
def delete_file_by_path(req: FilePathRequest, db: Session = Depends(get_db)):

db_file = db.scalar(
select(File).where(File.file_path == req.file_path)
)

if not db_file:
raise HTTPException(status_code=404, detail="File not found")

db.delete(db_file)
db.commit()

return {
"status": "success",
"message": "File deleted successfully."
}

class FileRenameRequest(BaseModel):
old_path: str
new_path: str

@router.post("/rename")
def rename_file(req: FileRenameRequest, db: Session = Depends(get_db)):

db_file = db.scalar(
select(File).where(File.file_path == req.old_path)
)

if not db_file:
raise HTTPException(status_code=404, detail="File not found")

db_file.file_path = req.new_path

db.commit()

return {
"status": "success",
"message": "File renamed successfully."
}

class FileCheckRequest(BaseModel):
file_path: str
file_hash: str

@router.post("/needs-indexing")
def needs_indexing(req: FileCheckRequest, db: Session = Depends(get_db)):
db_file = db.scalar(
select(File).where(
File.file_path == req.file_path
)
)

if db_file is None:
return {
"needs_indexing": True
}

if db_file.file_hash == req.file_hash:
return {
"needs_indexing": False
}

return {
"needs_indexing": True
}
8 changes: 5 additions & 3 deletions app/routes/upload.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import shutil

from fastapi import APIRouter, UploadFile, File, BackgroundTasks, HTTPException
from fastapi import APIRouter, Form, UploadFile, File, BackgroundTasks, HTTPException

from app.services.indexer import background_index_file

Expand All @@ -11,7 +11,8 @@
@router.post("/upload/")
async def upload_file(
background_tasks: BackgroundTasks,
file: UploadFile = File(...)
file: UploadFile = File(...),
original_path: str = Form(...)
):
"""
Accept a document and send it for background indexing.
Expand All @@ -27,7 +28,8 @@ async def upload_file(

background_tasks.add_task(
background_index_file,
temp_path
temp_path,
original_path
)

return {
Expand Down
95 changes: 95 additions & 0 deletions app/routes/watch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from pathlib import Path

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel

from app.database import SessionLocal
from app.models import WatchedFolder

router = APIRouter(tags=["Watch"])


class WatchFolderRequest(BaseModel):
folder_path: str


@router.post("/watch/folder")
def add_watch_folder(req: WatchFolderRequest):

db = SessionLocal()

try:
existing = db.query(WatchedFolder).filter_by(
folder_path=req.folder_path
).first()

if existing:
raise HTTPException(
status_code=409,
detail="Folder is already being watched."
)

watched_folder = WatchedFolder(
folder_path=req.folder_path
)

db.add(watched_folder)
db.commit()
db.refresh(watched_folder)

return {
"success": True,
"id": watched_folder.id,
"folder_path": watched_folder.folder_path
}

finally:
db.close()


@router.get("/watch/folders")
def get_watched_folders():

db = SessionLocal()

try:
folders = db.query(WatchedFolder).all()

return [
{
"id": folder.id,
"folder_path": folder.folder_path,
"created_at": folder.created_at
}
for folder in folders
]

finally:
db.close()


@router.delete("/watch/folder")
def remove_watch_folder(req: WatchFolderRequest):

db = SessionLocal()

try:
folder = db.query(WatchedFolder).filter_by(
folder_path=req.folder_path
).first()

if not folder:
raise HTTPException(
status_code=404,
detail="Folder not found."
)

db.delete(folder)
db.commit()

return {
"success": True
}

finally:
db.close()
21 changes: 21 additions & 0 deletions app/services/archive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import os
import tempfile
import zipfile


def extract_zip(zip_path: str):

temp_dir = tempfile.mkdtemp()

with zipfile.ZipFile(zip_path, "r") as archive:
archive.extractall(temp_dir)

extracted_files = []

for root, _, files in os.walk(temp_dir):
for file in files:
extracted_files.append(
os.path.join(root, file)
)

return temp_dir, extracted_files
Loading
Loading