dpv007 commited on
Commit
bed8441
·
1 Parent(s): ecc1150

Add Docker deployment configuration for Hugging Face

Browse files
Files changed (5) hide show
  1. .gitignore +0 -12
  2. Dockerfile +0 -39
  3. README.md +0 -107
  4. keystone-backend/.gitignore +2 -1
  5. keystone-backend/main.py +30 -11
.gitignore DELETED
@@ -1,12 +0,0 @@
1
- # Environments
2
- .env
3
- venv/
4
- __pycache__/
5
-
6
- # Node
7
- node_modules/
8
- .expo/
9
-
10
- # OS Files
11
- .DS_Store
12
- Thumbs.db
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,39 +0,0 @@
1
- FROM node:20 AS frontend
2
-
3
- WORKDIR /frontend
4
-
5
- COPY mobile/package.json ./
6
- RUN npm install
7
-
8
- COPY mobile/ .
9
- RUN npx expo export --platform web
10
-
11
- FROM python:3.11-slim
12
-
13
- RUN apt-get update && apt-get install -y --no-install-recommends \
14
- libmagic1 \
15
- libmagic-dev \
16
- libjpeg-dev \
17
- zlib1g-dev \
18
- libwebp-dev \
19
- curl \
20
- && rm -rf /var/lib/apt/lists/*
21
-
22
- WORKDIR /app
23
-
24
- COPY backend/requirements.txt ./requirements.txt
25
- RUN pip install --no-cache-dir --upgrade pip && \
26
- pip install --no-cache-dir -r requirements.txt
27
-
28
- COPY backend/ .
29
- COPY --from=frontend /frontend/dist ./web
30
-
31
- RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
32
- USER appuser
33
-
34
- EXPOSE 7860
35
-
36
- HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
37
- CMD curl -f http://localhost:7860/health || exit 1
38
-
39
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -8,110 +8,3 @@ app_port: 7860
8
  pinned: false
9
  license: mit
10
  ---
11
-
12
- # KeyStone 🔷 – Private Photo Cloud
13
-
14
- > Your photos. Your cloud. Zero dependencies on Google or Apple.
15
-
16
- KeyStone is a self-hosted Google Photos alternative with automatic mobile backup, deduplication, and AI-ready architecture.
17
-
18
- ## Architecture
19
-
20
- ```
21
- Expo App (iOS + Android + Web)
22
-
23
- Supabase Auth (JWT)
24
-
25
- FastAPI Backend ─────► Hugging Face Bucket (storage)
26
-
27
- Supabase PostgreSQL
28
- ```
29
-
30
- ## Live Deployment
31
-
32
- | Service | URL |
33
- |---|---|
34
- | 🚀 Backend API | https://dpv007-keystone.hf.space |
35
- | 📖 API Docs | https://dpv007-keystone.hf.space/docs |
36
-
37
- ## Quick Start
38
-
39
- ### Backend (local dev)
40
-
41
- ```bash
42
- cd backend
43
- cp .env.example .env # fill in your credentials
44
- docker compose up # starts FastAPI + PostgreSQL
45
- ```
46
-
47
- ### Run database migrations
48
-
49
- ```bash
50
- cd backend
51
- alembic upgrade head
52
- ```
53
-
54
- ### Mobile App
55
-
56
- ```bash
57
- cd mobile
58
- cp .env.example .env # fill in Supabase keys
59
- npm install
60
- npm start # Expo dev server (iOS/Android/Web)
61
- ```
62
-
63
- Open in browser: http://localhost:8081 (web), or scan QR in Expo Go.
64
-
65
- ## Environment Variables
66
-
67
- ### Backend (`backend/.env`)
68
-
69
- | Variable | Description |
70
- |---|---|
71
- | `DATABASE_URL` | Supabase PostgreSQL connection string |
72
- | `SUPABASE_URL` | Your Supabase project URL |
73
- | `SUPABASE_JWT_SECRET` | JWT secret from Supabase Dashboard → Settings → API |
74
- | `HF_TOKEN` | Hugging Face token with write access |
75
- | `HF_DATASET_REPO` | HF Dataset repo for photo storage (e.g. `user/keystone-photos`) |
76
-
77
- ### Mobile (`mobile/.env`)
78
-
79
- | Variable | Description |
80
- |---|---|
81
- | `EXPO_PUBLIC_API_URL` | Backend URL (defaults to HF Space URL) |
82
- | `EXPO_PUBLIC_SUPABASE_URL` | Your Supabase project URL |
83
- | `EXPO_PUBLIC_SUPABASE_ANON_KEY` | Supabase anon key |
84
-
85
- ## Features
86
-
87
- - ✅ Email/password authentication via Supabase
88
- - ✅ JWT verification on every API request
89
- - ✅ SHA256 deduplication (never upload the same photo twice)
90
- - ✅ Background sync (iOS + Android) with Wi-Fi-only option
91
- - ✅ Battery-aware syncing
92
- - ✅ Thumbnail generation (Pillow)
93
- - ✅ Infinite-scroll gallery
94
- - ✅ Albums with photo management
95
- - ✅ Photo search (filename + AI tags)
96
- - ✅ Favorites
97
- - ✅ Soft delete
98
- - ✅ Works on iOS, Android, and Web (same codebase)
99
- - 🔜 CLIP semantic search
100
- - 🔜 Face clustering
101
- - 🔜 OCR text extraction
102
- - 🔜 Auto albums
103
-
104
- ## Deployment to Hugging Face Spaces
105
-
106
- The backend is a Docker Space. Push this repo and set the following secrets in the Space settings:
107
-
108
- - `DATABASE_URL`
109
- - `SUPABASE_URL`
110
- - `SUPABASE_JWT_SECRET`
111
- - `SUPABASE_SERVICE_ROLE_KEY`
112
- - `HF_TOKEN`
113
- - `HF_DATASET_REPO`
114
-
115
- ## License
116
-
117
- MIT
 
8
  pinned: false
9
  license: mit
10
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
keystone-backend/.gitignore CHANGED
@@ -36,7 +36,8 @@ yarn-error.*
36
  venv/
37
  # typescript
38
  *.tsbuildinfo
39
-
 
40
  # generated native folders
41
  /ios
42
  /android
 
36
  venv/
37
  # typescript
38
  *.tsbuildinfo
39
+ # Local Storage
40
+ data/
41
  # generated native folders
42
  /ios
43
  /android
keystone-backend/main.py CHANGED
@@ -1,5 +1,6 @@
1
  import os
2
- from fastapi import FastAPI, HTTPException, Header, Depends
 
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pydantic import BaseModel
5
  from dotenv import load_dotenv
@@ -9,11 +10,20 @@ load_dotenv()
9
 
10
  app = FastAPI(title="KeyStone API", version="0.1.0")
11
 
12
- # Fetch credentials from environment variables
13
  DEV_USERNAME = os.getenv("DEV_USERNAME", "admin")
14
  DEV_PASSWORD = os.getenv("DEV_LOGIN_PASSWORD", "password123")
15
  MOCK_TOKEN = "mock-jwt-token-xyz123"
16
 
 
 
 
 
 
 
 
 
 
17
  app.add_middleware(
18
  CORSMiddleware,
19
  allow_origins=["*"],
@@ -22,7 +32,6 @@ app.add_middleware(
22
  allow_headers=["*"],
23
  )
24
 
25
- # Updated schema to look for username instead of email
26
  class LoginPayload(BaseModel):
27
  username: str
28
  password: str
@@ -38,24 +47,34 @@ def verify_mock_token(authorization: str = Header(None)):
38
 
39
  @app.get("/health")
40
  async def health_check():
41
- return {"status": "ok", "service": "KeyStone API"}
42
 
43
  @app.post("/auth/login")
44
  async def mock_login(payload: LoginPayload):
45
- # Strict matching for both username AND password
46
  if payload.username == DEV_USERNAME and payload.password == DEV_PASSWORD:
47
  return {
48
  "access_token": MOCK_TOKEN,
49
  "token_type": "bearer",
50
  "user": {"username": payload.username, "id": "mock-uuid-1111-2222"}
51
  }
52
-
53
- # Generic error message so we don't leak which field was incorrect
54
  raise HTTPException(status_code=400, detail="Incorrect username or password")
55
 
56
- @app.get("/photos")
57
- async def get_photos(token: str = Depends(verify_mock_token)):
 
 
 
 
 
 
 
 
 
 
 
 
58
  return {
59
- "message": "Authenticated successfully!",
60
- "photos": []
 
61
  }
 
1
  import os
2
+ import shutil
3
+ from fastapi import FastAPI, HTTPException, Header, Depends, UploadFile, File
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pydantic import BaseModel
6
  from dotenv import load_dotenv
 
10
 
11
  app = FastAPI(title="KeyStone API", version="0.1.0")
12
 
13
+ # Fetch credentials
14
  DEV_USERNAME = os.getenv("DEV_USERNAME", "admin")
15
  DEV_PASSWORD = os.getenv("DEV_LOGIN_PASSWORD", "password123")
16
  MOCK_TOKEN = "mock-jwt-token-xyz123"
17
 
18
+ # --- PERSISTENT STORAGE SETUP ---
19
+ # If running in HF Spaces with persistent storage, it mounts to /data.
20
+ # Locally, we will default to creating a folder called "data/photos" in the project root.
21
+ BASE_STORAGE_DIR = os.getenv("STORAGE_DIR", "./data/photos")
22
+
23
+ # Ensure the directory exists when the server starts
24
+ os.makedirs(BASE_STORAGE_DIR, exist_ok=True)
25
+ # --------------------------------
26
+
27
  app.add_middleware(
28
  CORSMiddleware,
29
  allow_origins=["*"],
 
32
  allow_headers=["*"],
33
  )
34
 
 
35
  class LoginPayload(BaseModel):
36
  username: str
37
  password: str
 
47
 
48
  @app.get("/health")
49
  async def health_check():
50
+ return {"status": "ok", "service": "KeyStone API", "storage_path": BASE_STORAGE_DIR}
51
 
52
  @app.post("/auth/login")
53
  async def mock_login(payload: LoginPayload):
 
54
  if payload.username == DEV_USERNAME and payload.password == DEV_PASSWORD:
55
  return {
56
  "access_token": MOCK_TOKEN,
57
  "token_type": "bearer",
58
  "user": {"username": payload.username, "id": "mock-uuid-1111-2222"}
59
  }
 
 
60
  raise HTTPException(status_code=400, detail="Incorrect username or password")
61
 
62
+ # --- NEW UPLOAD ENDPOINT ---
63
+ @app.post("/upload")
64
+ async def upload_photo(file: UploadFile = File(...), token: str = Depends(verify_mock_token)):
65
+ # Create the full file path safely
66
+ file_path = os.path.join(BASE_STORAGE_DIR, file.filename)
67
+
68
+ try:
69
+ # Save the file in chunks to keep memory usage low
70
+ with open(file_path, "wb") as buffer:
71
+ while content := await file.read(1024 * 1024): # Read in 1MB chunks
72
+ buffer.write(content)
73
+ except Exception as e:
74
+ raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}")
75
+
76
  return {
77
+ "message": "Photo uploaded successfully!",
78
+ "filename": file.filename,
79
+ "path": file_path
80
  }