journal: paste image from clipboard, cap embedded image size, fix media persistence
- Paste-to-embed: pasting an image into the markdown editor uploads it and
inserts  at the cursor. Unlike gallery attachments these aren't
tied to a journal_entry (the entry may not exist yet while composing), so
they're stored per-user under app/media/journal/{user_id}/pasted/ with no
DB row, served through an ownership-scoped route, and never cleaned up
automatically when an entry is deleted -- an accepted tradeoff at this
app's personal scale.
- The markdown sanitizer was stripping all <img> tags (not on the bleach
allowlist), which would have silently deleted every pasted image on save;
added img/src/alt/title while keeping event-handler attributes blocked.
- Cap embedded image width in both the editor pane and the rendered preview
so a large pasted photo can't overflow its card.
- Fix real data loss risk found while testing this: docker-compose.yml had
no volume for app/media, so every container recreate during a deploy wiped
uploaded photos, and deploy_sftp.py was syncing app/media/ (runtime user
data, not source) into the remote build context. Added the volume mount
and excluded media/ from the sync script. Recovered and relocated the
real attachments that had already landed in the wrong place on the NAS
during earlier deploys this session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -294,6 +294,41 @@ def delete_attachment(db: Session, attachment: JournalAttachment) -> None:
|
||||
db.commit()
|
||||
|
||||
|
||||
# ---- 에디터에 붙여넣은 이미지 ----
|
||||
# 글을 쓰는 중(아직 엔트리가 저장되기 전)에 클립보드로 붙여넣은 이미지라 JournalAttachment처럼
|
||||
# entry_id에 묶을 수가 없다 — DB 행 없이 유저별 폴더에만 저장하고, 마크다운 본문에
|
||||
#  형태로 직접 참조한다. 그래서 첨부파일 갤러리(삭제 버튼 등)에는 안 뜨고, 엔트리를
|
||||
# 지워도 자동으로 같이 지워지지 않는다(개인 규모 사용량이라 감수할 만한 트레이드오프).
|
||||
def pasted_image_dir(user_id: int) -> Path:
|
||||
return Path(settings.journal_media_root) / str(user_id) / "pasted"
|
||||
|
||||
|
||||
def save_pasted_image(user_id: int, upload_file: UploadFile) -> str:
|
||||
"""붙여넣은 이미지를 저장하고 파일명(서빙 URL에 쓸 값)을 반환한다."""
|
||||
content_type = upload_file.content_type or ""
|
||||
if content_type not in ALLOWED_IMAGE_TYPES:
|
||||
raise ValueError("이미지 파일만 붙여넣을 수 있어요 (jpg/png/webp/gif)")
|
||||
|
||||
data = upload_file.file.read()
|
||||
max_bytes = settings.journal_max_upload_mb * 1024 * 1024
|
||||
if len(data) > max_bytes:
|
||||
raise ValueError(f"파일 용량은 {settings.journal_max_upload_mb}MB를 넘을 수 없어요")
|
||||
|
||||
target_dir = pasted_image_dir(user_id)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
ext = mimetypes.guess_extension(content_type) or ".png"
|
||||
filename = f"{uuid.uuid4().hex}{ext}"
|
||||
(target_dir / filename).write_bytes(data)
|
||||
return filename
|
||||
|
||||
|
||||
def get_pasted_image_path(user_id: int, filename: str) -> Path | None:
|
||||
# Path(...).name이 디렉터리 구분자를 전부 제거해줘서 "../"류 경로 탈출을 막아준다.
|
||||
safe_name = Path(filename).name
|
||||
path = pasted_image_dir(user_id) / safe_name
|
||||
return path if path.is_file() else None
|
||||
|
||||
|
||||
# ---- 캘린더 / day-detail / 회상 ----
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user