diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml new file mode 100644 index 0000000..219a84f --- /dev/null +++ b/.gitea/workflows/docker-build.yml @@ -0,0 +1,179 @@ +name: Build & Release + +on: + push: + branches: [master, main] + tags: ['v*'] + pull_request: + branches: [master, main] + +env: + REGISTRY: git.necloud.us + OWNER: nate + REPO: cloudlink + +jobs: + # ─── Docker: Backend ──────────────────────────────────────────────────────── + build-backend: + name: Docker / backend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: | + ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:latest + ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:cache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/backend:cache,mode=max + + # ─── Docker: Frontend ─────────────────────────────────────────────────────── + build-frontend: + name: Docker / frontend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USER }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + build-args: | + VITE_API_URL=/api + VITE_WS_URL=/ws + tags: | + ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:latest + ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:${{ github.sha }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:cache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.REPO }}/frontend:cache,mode=max + + # ─── Desktop installers (Linux + Windows, built on Linux runner) ───────────── + build-desktop: + name: Desktop / Linux & Windows + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: desktop/package-lock.json + + - name: Install desktop dependencies + working-directory: desktop + run: npm ci + + - name: Install Wine (Windows cross-compile) + run: | + sudo dpkg --add-architecture i386 + sudo apt-get update -q + sudo apt-get install -y --no-install-recommends wine wine32 wine64 + + - name: Build Linux AppImage + working-directory: desktop + env: + # electron-builder reads this to avoid interactive prompts + GH_TOKEN: unused + run: npx electron-builder --linux AppImage --x64 + + - name: Build Windows NSIS installer + working-directory: desktop + env: + GH_TOKEN: unused + run: npx electron-builder --win nsis --x64 + + - uses: actions/upload-artifact@v4 + with: + name: desktop-linux-windows-${{ github.sha }} + path: | + desktop/dist/*.AppImage + desktop/dist/*.exe + if-no-files-found: warn + retention-days: 30 + + # ─── Deploy (on push to master only, not PRs or tags) ─────────────────────── + deploy: + name: Deploy to server + runs-on: ubuntu-latest + needs: [build-backend, build-frontend] + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') + steps: + - name: SSH deploy + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + script: | + cd ${{ secrets.DEPLOY_PATH }} + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.necloud.us -u "${{ secrets.REGISTRY_USER }}" --password-stdin + docker compose pull backend frontend + docker compose up -d --no-build backend frontend + docker logout git.necloud.us + docker image prune -f + + # ─── Release (only on v* tags) ─────────────────────────────────────────────── + release: + name: Create release + runs-on: ubuntu-latest + needs: [build-desktop, build-backend, build-frontend] + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Download desktop artifacts + uses: actions/download-artifact@v4 + with: + name: desktop-linux-windows-${{ github.sha }} + path: ./release-assets + + - name: Publish Gitea release + env: + GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + TAG: ${{ github.ref_name }} + SHA: ${{ github.sha }} + run: | + # Create the release + RELEASE=$(curl -sf -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + "https://git.necloud.us/api/v1/repos/nate/cloudlink/releases" \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"CloudLink $TAG\",\"body\":\"Docker images pushed to registry. Desktop installers attached below.\"}") + + RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") + echo "Release ID: $RELEASE_ID" + + # Upload each installer as a release asset + for file in ./release-assets/*; do + NAME=$(basename "$file") + echo "Uploading $NAME..." + curl -sf -X POST \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + "https://git.necloud.us/api/v1/repos/nate/cloudlink/releases/$RELEASE_ID/assets?name=$NAME" \ + --data-binary @"$file" + done diff --git a/docker-compose.yml b/docker-compose.yml index d1004bd..b98ef7b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: retries: 5 backend: + image: git.necloud.us/nate/cloudlink/backend:latest build: context: ./backend dockerfile: Dockerfile @@ -39,6 +40,7 @@ services: condition: service_healthy frontend: + image: git.necloud.us/nate/cloudlink/frontend:latest build: context: ./frontend dockerfile: Dockerfile diff --git a/frontend/src/pages/DrivePage.tsx b/frontend/src/pages/DrivePage.tsx index ed8df32..cd9173e 100644 --- a/frontend/src/pages/DrivePage.tsx +++ b/frontend/src/pages/DrivePage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, useEffect } from 'react'; +import { useRef, useState, useEffect, useCallback } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { Upload, File, Trash2, Download, Lock } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; @@ -34,6 +34,7 @@ export default function DrivePage() { const qc = useQueryClient(); const inputRef = useRef(null); const [uploading, setUploading] = useState(false); + const [dragOver, setDragOver] = useState(false); const [decryptedNames, setDecryptedNames] = useState>({}); const { data: files = [] } = useQuery({ @@ -60,11 +61,9 @@ export default function DrivePage() { })(); }, [files, masterKey]); - async function handleUpload(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - if (!file || !masterKey) return; + const uploadFile = useCallback(async (file: File) => { + if (!masterKey) return; setUploading(true); - try { const fileKey = await generateFileKey(); const encryptedBlob = await encryptFile(file, fileKey); @@ -83,6 +82,18 @@ export default function DrivePage() { setUploading(false); if (inputRef.current) inputRef.current.value = ''; } + }, [masterKey, qc]); + + function handleUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (file) uploadFile(file); + } + + function handleDrop(e: React.DragEvent) { + e.preventDefault(); + setDragOver(false); + const file = e.dataTransfer.files[0]; + if (file) uploadFile(file); } async function handleDownload(f: DriveFile) { @@ -125,11 +136,25 @@ export default function DrivePage() { -
+
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + > + {dragOver && ( +
+
+ +

Drop to encrypt & upload

+
+
+ )} + {files.length === 0 ? (
-

No files yet — upload your first encrypted file

+

No files yet — drag & drop or click Upload

) : (