commit ad91226e3f278bf34597cd8bae88fd6170064eb5 Author: Godopu Date: Thu Feb 19 11:00:53 2026 +0900 init diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..66708a9 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(python3 -m venv venv)", + "Bash(./venv/bin/pip install:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd5c9e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +assets/images/* +assets/videos/* \ No newline at end of file diff --git a/PROMPT-OLD.md b/PROMPT-OLD.md new file mode 100644 index 0000000..2e0f5e5 --- /dev/null +++ b/PROMPT-OLD.md @@ -0,0 +1,25 @@ +# 프로젝트 구현 기록 (Prompts) - 원본 + +본 파일은 프로젝트 진행 과정에서 전달된 사용자의 주요 요구 사항들을 시간 순서대로 정리한 기록입니다. + +### 1. 프로젝트 소개 파일 수정 +- **요구 사항**: `README.md` 파일에는 구현할 프로그램의 설명과 요구사항이 작성되어 있어. 견고한 프로그램 구현을 위해 해당 파일의 내용을 수정해서 `REQUIREMENTS.md` 파일에 저장해줘. + +### 2. 프로젝트 구현 +- **요구 사항**: 작성된 `REQUIREMENTS.md` 파일의 내용을 참고해서 프로그램 초안을 구현해줘. + +### 3. 초기 레포트 생성 및 분석 +- **요구 사항**: 구현한 프로그램에 대한 설명, 실행법, curl을 이용한 테스트 방법 등의 내용을 `REPORT.md` 파일에 작성해줘. + +### 4. YouTube 다운로드 기능 추가 +- **요구 사항**: youtube 링크를 전달하면 해당 링크에서 파일을 다운로드 할 수 있도록 youtube 링크 전달 및 다운로드를 위한 API를 만들어줘. 그리고 이를 이용할 수 있도록 web interface를 수정, `REPORT.md` 파일 또한 함께 수정해줘. + +### 5. 실시간 스트리밍 지원 +- **요구 사항**: 다운로드 받은 영상의 경우 실시간 스트리밍이 가능하도록 코드를 변경해줘. + +### 6. 웹 UI 모던화 및 스타일 개선 +- **요구 사항**: web ui가 너무 심플하고, 사용하기 불편해. 모던하고, 파스텔 컬러 스타일의 사용하기 편한 UI로 웹을 전반적으로 수정해줘. + +### 7. 파일 삭제 기능 구현 +- **요구 사항**: 업로드한 파일을 삭제할 수 있는 기능을 작성해줘. + diff --git a/PROMPT.md b/PROMPT.md new file mode 100644 index 0000000..1da3d8d --- /dev/null +++ b/PROMPT.md @@ -0,0 +1,63 @@ +# 효율적인 프롬프트 작성 가이드 및 프로젝트 기록 + +이 문서는 프로젝트 개발 과정에서의 요구사항을 **Prompt Engineering** 관점에서 재구성한 가이드입니다. 각 기능 구현 시 모델에게 전달하면 가장 효과적인 구조를 채택했습니다. + +--- + +### 💡 시스템 기본 페르소나 (System Role) +> "너는 FastAPI와 현대적인 Frontend 기술에 능숙한 시니어 풀스택 개발자야. 보안(경로 탐색 방지), 코드 효율성, 그리고 사용자 친화적인 UI/UX를 최우선으로 고려해서 코드를 작성해줘." + +--- + +### 1. 요구사항 분석 및 구체화 (Requirement Engineering) +**[Prompt]** +- **Context**: `README.md` 파일에 프로그램 설명과 기초 요구사항이 작성되어 있어. +- **Task**: 견고하고 전문적인 프로그램 구현을 위해 이 내용을 분석하고, 상세한 소프트웨어 요구사항 명세서(SRS)를 작성해서 `REQUIREMENTS.md` 파일에 저장해줘. +- **Output**: 기능적/비기능적 요구사항, 기술 스택, 디렉토리 구조가 포함된 명세서. + +### 2. 초기 프로토타입 구현 (Prototype Implementation) +**[Prompt]** +- **Context**: 작성된 `REQUIREMENTS.md` 명세서를 기반으로 프로젝트를 시작하려 해. +- **Task**: 명세서의 모든 요구사항을 충족하는 FastAPI 애플리케이션의 초안 코드를 작성해줘. +- **Constraints**: + - 파일 업로드/목록 조회/다운로드 API 포함. + - 파일 타입별(이미지/비디오) 자동 분류 저장 로직 구현. + - 메타데이터는 `metadata.json`으로 관리할 것. + +### 3. 프로젝트 분석 및 문서화 (Documentation) +**[Prompt]** +- **Task**: 현재 구현된 코드베이스를 분석하고 사용자용 `REPORT.md`를 작성해줘. +- **Instructions**: + 1. 핵심 기능 요약 및 실행 방법 안내. + 2. `curl`을 이용한 API 엔드포인트별 테스트 명령어 예시 포함. + +### 4. YouTube 연동 기능 (External Integration) +**[Prompt]** +- **Task**: YouTube URL을 통해 영상을 서버로 직접 다운로드하는 기능을 추가해줘. +- **Requirements**: + - `yt-dlp` 라이브러리를 사용하고, 결과물을 `assets/videos`에 저장. + - Web UI에 URL 입력 폼과 처리 상태 표시 기능을 통합할 것. + +### 5. 스트리밍 엔진 최적화 (Streaming & Range Requests) +**[Prompt]** +- **Task**: 대용량 영상의 실시간 스트리밍(Seeking)이 가능하도록 개선해줘. +- **Technical Detail**: FastAPI의 `FileResponse`가 HTTP Range Requests를 지원하도록 설정하고, 브라우저 내 재생을 위해 `inline` 콘텐츠 배치를 적용할 것. + +### 6. UI/UX 현대화 (Frontend Redesign) +**[Prompt]** +- **Role**: 전문 UI/UX 디자이너 및 프론트엔드 개발자. +- **Task**: 기존의 기본적인 UI를 현대적인 파스텔 톤 스타일로 전면 개편해줘. +- **Style Guide**: 카드형 레이아웃, Google Fonts (Poppins), 반응형 디자인, 작업 상태 애니메이션(Loader) 적용. + +### 7. 리소스 관리 및 삭제 (Resource Management) +**[Prompt]** +- **Task**: 서버 리소스 관리를 위해 파일 삭제 기능을 추가해줘. +- **Constraints**: 실제 파일 삭제와 메타데이터 제거를 동시 처리하고, UI에서는 삭제 전 사용자 확인 절차를 둘 것. + +--- + +### 🚀 효율적인 프롬프트 전달 팁 +1. **역할 정의 (Role)**: 전문가 페르소나를 지정하세요. +2. **구체적인 제약 조건 (Constraints)**: 기술적 한계나 필수 라이브러리를 명시하세요. +3. **단계별 지시 (Step-by-Step)**: 복잡한 작업은 논리적 순서로 나누어 요청하세요. +4. **출력 형식 지정 (Output Format)**: 결과물의 형태(Markdown, JSON, 특정 코드 스타일 등)를 정의하세요. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9757e9f --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# 프로젝트 개요 +- fastapi를 이용한 파일 업로드/다운로드 프로그램 작성 + +# 프로젝트 요구사항 + +- fastapi를 통해 파일 업로드 및 다운로드를 위한 api 구현 +- 업로드한 파일은 파일의 타입에 따라 assets/videos 또는 assets/images에 저장 +- 저장된 파일의 정보 (파일 이름, 타입, 업로드 날짜)는 별도 json 파일을 이용하여 관리 +- API 인터페이스 정의 + - 파일 업로드: /api/files/upload + - 파일 다운로드: /api/files/download + - static (웹 (html, css, javascript) 서빙) : /web + + + diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..1b05238 --- /dev/null +++ b/REPORT.md @@ -0,0 +1,99 @@ +# 프로젝트 결과 보고서: FastAPI 파일 관리 시스템 + +## 1. 프로그램 개요 +본 프로그램은 **FastAPI**를 기반으로 한 웹 기반 파일 관리 시스템입니다. 사용자는 이미와 동영상 파일을 업로드하고, 서버에서 해당 파일을 다운로드하거나 목록을 확인할 수 있습니다. + +### 주요 기능 +- **파일 업로드 (`POST /api/files/upload`)**: 파일을 업로드하면 MIME 타입에 따라 `assets/images` 또는 `assets/videos` 폴더로 자동 분류하여 저장합니다. +- **파일 목록 조회 (`GET /api/files`)**: 서버에 저장된 모든 파일의 메타데이터(파일명, 타입, 업로드 날짜, 경로 등)를 JSON 형식으로 반환합니다. +- **파일 다운로드 (`GET /api/files/download`)**: 파일명을 파라미터로 전달하여 서버에 저장된 파일을 로컬로 다운로드할 수 있습니다. +- **파일 삭제 (`DELETE /api/files`)**: 서버에서 특정 파일을 삭제하고 메타데이터에서도 제거합니다. +- **메타데이터 관리**: 업로드된 파일의 정보는 `metadata.json` 파일에 기록되어 영구적으로 관리됩니다. +- **YouTube 다운로드 (`POST /api/files/youtube`)**: YouTube 링크를 전달받아 해당 영상을 서버로 다운로드하고 목록에 추가합니다. +- **실시간 스트리밍 지원**: `FileResponse`의 Range Requests 지원을 통해 대용량 영상 파일도 브라우저에서 원하는 시점으로 이동하며 끊김 없이 감상할 수 있습니다. +- **모던 파스텔 UI**: 사용자 친화적인 파스텔 톤의 카드 레이아웃과 반응형 디자인을 적용하여 모바일과 데스크톱 모두에서 쾌적하게 사용할 수 있습니다. +- **웹 인터페이스 서빙 (`/web`)**: `static/` 디렉토리의 HTML/JS 파일을 통해 사용자에게 웹 화면을 제공합니다. + +--- + +## 2. 실행 방법 + +### 환경 준비 +Python 3.9 버전 이상이 필요합니다. + +1. **가상환경 생성 및 활성화** + ```bash + python -m venv .venv + source .venv/bin/activate # Linux/macOS + # 또는 + .venv\Scripts\activate # Windows + ``` + +2. **필수 패키지 설치** + ```bash + pip install fastapi uvicorn python-multipart yt-dlp + ``` + +### 서버 실행 +```bash +python main.py +``` +서버는 기본적으로 `http://0.0.0.0:8000`에서 실행됩니다. + +--- + +## 3. curl을 이용한 테스트 방법 + +### A. 파일 업로드 테스트 +`assets/images/dummy.png` 파일을 업로드하는 예시입니다. + +```bash +curl -X POST "http://localhost:8000/api/files/upload" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "file=@assets/images/dummy.png" \ + -F "description=테스트 이미지입니다" \ + -F "uploader=홍길동" +``` + +### B. 파일 목록 조회 테스트 +서버에 저장된 모든 파일의 정보를 확인합니다. + +```bash +curl -X GET "http://localhost:8000/api/files" +``` + +### C. 파일 다운로드 테스트 +`dummy.png`라는 이름의 파일을 다운로드하여 `downloaded_file.png`로 저장하는 예시입니다. + +```bash +curl -X GET "http://localhost:8000/api/files/download?filename=dummy.png" \ + --output downloaded_file.png +``` + +### D. 파일 삭제 테스트 +서버의 파일을 삭제하는 예시입니다. + +```bash +curl -X DELETE "http://localhost:8000/api/files?filename=dummy.png" +``` + +### E. YouTube 다운로드 테스트 +YouTube 링크를 통해 영상을 다운로드하는 예시입니다. + +```bash +curl -X POST "http://localhost:8000/api/files/youtube" \ + -H "accept: application/json" \ + -H "Content-Type: multipart/form-data" \ + -F "url=https://www.youtube.com/watch?v=EXAMPLE_ID" \ + -F "uploader=테스터" +``` + +--- + +## 4. 디렉토리 구조 +- `main.py`: FastAPI 애플리케이션 엔트리 포인트 +- `metadata.json`: 파일 메타데이터 저장소 +- `assets/`: 업로드된 실제 파일 저장 (images/videos 분류) +- `static/`: 웹 프론트엔드 정적 파일 +- `REQUIREMENTS.md`: 프로젝트 요구사항 명세서 diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md new file mode 100644 index 0000000..b33ebce --- /dev/null +++ b/REQUIREMENTS.md @@ -0,0 +1,69 @@ +# Detailed Software Requirements Specification + +## 1. Project Overview +A web-based file management system using FastAPI that allows users to upload and download files. The system categorizes files by type and maintains metadata in a JSON file. + +## 2. Functional Requirements + +### 2.1 File Upload +- **Endpoint**: `POST /api/files/upload` +- **Description**: Accepts a file and its associated metadata. +- **Payload**: Multipart form-data containing: + - `file`: The actual file to upload. + - `description`: (Optional) A brief description of the file. + - `uploader`: (Optional) Name of the person uploading. +- **Storage Logic**: + - Images (e.g., JPG, PNG, GIF) must be saved in `assets/images/`. + - Videos (e.g., MP4, AVI, MKV) must be saved in `assets/videos/`. + - *Optional: Handle other file types or return an error if unsupported.* +- **Metadata Management**: + - Upon successful upload, record the following in `metadata.json`: + - `filename`: Original or sanitized filename. + - `type`: MIME type or category (image/video). + - `upload_date`: ISO 8601 formatted timestamp. + - `file_path`: Relative path to the stored file. + - `description`: Provided description. + - `uploader`: Provided uploader name. + +### 2.2 File List Retrieval +- **Endpoint**: `GET /api/files` +- **Description**: Returns a list of all uploaded files and their metadata. +- **Response**: JSON array containing metadata for each file stored in `metadata.json`. + +### 2.3 File Download +- **Endpoint**: `GET /api/files/download` +- **Parameters**: `filename` (query parameter). +- **Description**: Streams the requested file to the client. +- **Error Handling**: Return 404 if the file is not found in metadata or on disk. + +### 2.3 Static Web Serving +- **Endpoint**: `/web` +- **Description**: Serve static HTML, CSS, and JavaScript files from a `static` directory to provide a user interface. + +## 3. Technical Requirements + +### 3.1 Backend +- **Framework**: FastAPI +- **Language**: Python 3.9+ +- **Dependencies**: `python-multipart` (for file uploads), `uvicorn` (ASGI server). + +### 3.2 Data Storage +- **Filesystem**: Local storage under `assets/` directory. +- **Metadata**: A single `metadata.json` file acting as a lightweight database. + +### 3.3 Directory Structure +``` +. +├── main.py # FastAPI application entry point +├── metadata.json # File metadata storage +├── assets/ +│ ├── images/ # Uploaded image files +│ └── videos/ # Uploaded video files +├── static/ # Web frontend files (HTML/CSS/JS) +└── REQUIREMENTS.md # This document +``` + +## 4. Non-Functional Requirements +- **Security**: Sanitize filenames to prevent path traversal attacks. +- **Performance**: Efficiently handle file streaming for downloads. +- **Maintainability**: Clear separation between API logic and file management. diff --git a/backup.md b/backup.md new file mode 100644 index 0000000..9757e9f --- /dev/null +++ b/backup.md @@ -0,0 +1,15 @@ +# 프로젝트 개요 +- fastapi를 이용한 파일 업로드/다운로드 프로그램 작성 + +# 프로젝트 요구사항 + +- fastapi를 통해 파일 업로드 및 다운로드를 위한 api 구현 +- 업로드한 파일은 파일의 타입에 따라 assets/videos 또는 assets/images에 저장 +- 저장된 파일의 정보 (파일 이름, 타입, 업로드 날짜)는 별도 json 파일을 이용하여 관리 +- API 인터페이스 정의 + - 파일 업로드: /api/files/upload + - 파일 다운로드: /api/files/download + - static (웹 (html, css, javascript) 서빙) : /web + + + diff --git a/main.py b/main.py new file mode 100644 index 0000000..5789041 --- /dev/null +++ b/main.py @@ -0,0 +1,187 @@ +import json +import os +import shutil +import yt_dlp +from datetime import datetime +from pathlib import Path +from typing import List, Optional + +import yt_dlp +from fastapi import FastAPI, File, Form, HTTPException, UploadFile +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles + +app = FastAPI() + +# Configuration +BASE_DIR = Path(__file__).parent +ASSETS_DIR = BASE_DIR / "assets" +IMAGES_DIR = ASSETS_DIR / "images" +VIDEOS_DIR = ASSETS_DIR / "videos" +METADATA_FILE = BASE_DIR / "metadata.json" +STATIC_DIR = BASE_DIR / "static" + +# Ensure directories exist +for directory in [IMAGES_DIR, VIDEOS_DIR, STATIC_DIR]: + directory.mkdir(parents=True, exist_ok=True) + +if not METADATA_FILE.exists(): + METADATA_FILE.write_text("[]") + +# Mount static files +app.mount("/web", StaticFiles(directory=str(STATIC_DIR), html=True), name="static") + +def load_metadata() -> List[dict]: + try: + with open(METADATA_FILE, "r") as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return [] + +def save_metadata(metadata: List[dict]): + with open(METADATA_FILE, "w") as f: + json.dump(metadata, f, indent=4) + +def get_file_category(content_type: str) -> str: + if content_type.startswith("image/"): + return "images" + elif content_type.startswith("video/"): + return "videos" + return "others" + +def sanitize_filename(filename: str) -> str: + # Basic sanitization: remove path traversal attempts and keep only alphanumeric/dots/underscores + return os.path.basename(filename).replace(" ", "_") + +@app.post("/api/files/upload") +async def upload_file( + file: UploadFile = File(...), + description: Optional[str] = Form(None), + uploader: Optional[str] = Form(None) +): + category = get_file_category(file.content_type) + if category == "others": + raise HTTPException(status_code=400, detail="Unsupported file type. Only images and videos are allowed.") + + filename = sanitize_filename(file.filename) + target_dir = IMAGES_DIR if category == "images" else VIDEOS_DIR + file_path = target_dir / filename + + # Handle duplicate filenames by appending timestamp + if file_path.exists(): + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + name, ext = os.path.splitext(filename) + filename = f"{name}_{timestamp}{ext}" + file_path = target_dir / filename + + try: + with file_path.open("wb") as buffer: + shutil.copyfileobj(file.file, buffer) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}") + + metadata_entry = { + "filename": filename, + "type": file.content_type, + "upload_date": datetime.now().isoformat(), + "file_path": str(file_path.relative_to(BASE_DIR)), + "description": description, + "uploader": uploader + } + + metadata = load_metadata() + metadata.append(metadata_entry) + save_metadata(metadata) + + return metadata_entry + +@app.post("/api/files/youtube") +async def download_youtube( + url: str = Form(...), + description: Optional[str] = Form(None), + uploader: Optional[str] = Form(None) +): + ydl_opts = { + 'format': 'bestvideo+bestaudio/best', + 'outtmpl': str(VIDEOS_DIR / '%(title)s.%(ext)s'), + 'quiet': True, + 'no_warnings': True, + } + + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info = ydl.extract_info(url, download=True) + file_path_str = ydl.prepare_filename(info) + file_path = Path(file_path_str) + filename = file_path.name + + metadata_entry = { + "filename": filename, + "type": "video/mp4", # Simplified, yt-dlp might download different formats + "upload_date": datetime.now().isoformat(), + "file_path": str(file_path.relative_to(BASE_DIR)), + "description": description or info.get('title'), + "uploader": uploader or info.get('uploader') + } + + metadata = load_metadata() + metadata.append(metadata_entry) + save_metadata(metadata) + + return metadata_entry + except Exception as e: + raise HTTPException(status_code=500, detail=f"YouTube download failed: {str(e)}") + +@app.get("/api/files") +async def list_files(): + return load_metadata() + +@app.get("/api/files/download") +async def download_file(filename: str, stream: bool = False): + metadata = load_metadata() + entry = next((item for item in metadata if item["filename"] == filename), None) + + if not entry: + raise HTTPException(status_code=404, detail="File not found in metadata") + + file_path = BASE_DIR / entry["file_path"] + if not file_path.exists(): + raise HTTPException(status_code=404, detail="File not found on disk") + + # If stream is True, we use 'inline' so the browser can play it + disposition = "inline" if stream else "attachment" + + return FileResponse( + path=file_path, + filename=filename if not stream else None, + media_type=entry["type"], + content_disposition_type=disposition + ) + +@app.delete("/api/files") +async def delete_file(filename: str): + metadata = load_metadata() + entry_index = next((i for i, item in enumerate(metadata) if item["filename"] == filename), None) + + if entry_index is None: + raise HTTPException(status_code=404, detail="File not found in metadata") + + entry = metadata[entry_index] + file_path = BASE_DIR / entry["file_path"] + + # Delete from filesystem + try: + if file_path.exists(): + file_path.unlink() + except Exception as e: + raise HTTPException(status_code=500, detail=f"Could not delete file from disk: {str(e)}") + + # Remove from metadata + metadata.pop(entry_index) + save_metadata(metadata) + + return {"message": f"File {filename} deleted successfully"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..01cbcb2 --- /dev/null +++ b/metadata.json @@ -0,0 +1,26 @@ +[ + { + "filename": "dummy.png", + "type": "image/png", + "upload_date": "2026-02-19T09:45:18.881161", + "file_path": "assets/images/dummy.png", + "description": "Test image", + "uploader": "Claude" + }, + { + "filename": "dummy.mp4", + "type": "video/mp4", + "upload_date": "2026-02-19T09:45:57.924247", + "file_path": "assets/videos/dummy.mp4", + "description": "Test video", + "uploader": "Claude" + }, + { + "filename": "Elegant Jazz \uff5c Sophisticated Melodies in a Luxury Hotel Lobby #1.webm", + "type": "video/mp4", + "upload_date": "2026-02-19T10:41:57.544173", + "file_path": "assets/videos/Elegant Jazz \uff5c Sophisticated Melodies in a Luxury Hotel Lobby #1.webm", + "description": "Elegant Jazz | Sophisticated Melodies in a Luxury Hotel Lobby #1", + "uploader": "Blue in Green" + } +] \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..a6beafd --- /dev/null +++ b/static/index.html @@ -0,0 +1,301 @@ + + + + + + Modern File Manager + + + + +
+

✨ File Manager

+ + +
+ + +
+

Playing...

+
+
+ +
+ +
+

📁 Local Upload

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

📺 YouTube Link

+
+
+ + +
+
+ + +
+
+ +
+
+
+ +

📦 Library

+
+ +
+
+ + + +