feat: file edit added

This commit is contained in:
kodi
2026-03-11 14:09:44 +01:00
parent ba6a369f78
commit b93cb01879
18 changed files with 701 additions and 16 deletions
+73 -2
View File
@@ -3,11 +3,12 @@ from __future__ import annotations
from pathlib import Path
from backend.app.api.errors import AppError
from backend.app.api.schemas import DeleteResponse, MkdirResponse, RenameResponse, ViewResponse
from backend.app.api.schemas import DeleteResponse, MkdirResponse, RenameResponse, SaveResponse, ViewResponse
from backend.app.fs.filesystem_adapter import FilesystemAdapter
from backend.app.security.path_guard import PathGuard
TEXT_PREVIEW_MAX_BYTES = 256 * 1024
TEXT_EDIT_MAX_BYTES = 256 * 1024
TEXT_CONTENT_TYPES = {
".txt": "text/plain",
".log": "text/plain",
@@ -146,7 +147,7 @@ class FileOpsService:
return DeleteResponse(path=resolved_target.relative)
def view(self, path: str) -> ViewResponse:
def view(self, path: str, for_edit: bool = False) -> ViewResponse:
resolved_target = self._path_guard.resolve_existing_path(path)
if resolved_target.absolute.is_dir():
@@ -173,6 +174,14 @@ class FileOpsService:
details={"path": resolved_target.relative},
)
if for_edit and resolved_target.absolute.stat().st_size > TEXT_EDIT_MAX_BYTES:
raise AppError(
code="file_too_large",
message="File is too large for edit",
status_code=409,
details={"path": resolved_target.relative},
)
try:
preview = self._filesystem.read_text_preview(
resolved_target.absolute,
@@ -194,9 +203,71 @@ class FileOpsService:
encoding="utf-8",
truncated=preview["truncated"],
size=preview["size"],
modified=preview["modified"],
content=preview["content"],
)
def save(self, path: str, content: str, expected_modified: str) -> SaveResponse:
resolved_target = self._path_guard.resolve_existing_path(path)
if resolved_target.absolute.is_dir():
raise AppError(
code="type_conflict",
message="Source must be a file",
status_code=409,
details={"path": resolved_target.relative},
)
if not resolved_target.absolute.is_file():
raise AppError(
code="type_conflict",
message="Unsupported path type for save",
status_code=409,
details={"path": resolved_target.relative},
)
if self._content_type_for(resolved_target.absolute) is None:
raise AppError(
code="unsupported_type",
message="File type is not supported for edit",
status_code=409,
details={"path": resolved_target.relative},
)
if len(content.encode("utf-8")) > TEXT_EDIT_MAX_BYTES:
raise AppError(
code="file_too_large",
message="File is too large for edit",
status_code=409,
details={"path": resolved_target.relative},
)
current_modified = self._filesystem.modified_iso(resolved_target.absolute)
if current_modified != expected_modified:
raise AppError(
code="conflict",
message="File changed since it was opened",
status_code=409,
details={"path": resolved_target.relative},
)
try:
saved = self._filesystem.write_text_file(
resolved_target.absolute,
content=content,
encoding="utf-8",
)
except OSError as exc:
raise AppError(
code="io_error",
message="Filesystem operation failed",
status_code=500,
details={"reason": str(exc)},
)
return SaveResponse(
path=resolved_target.relative,
size=saved["size"],
modified=saved["modified"],
)
@staticmethod
def _join_relative(base: str, name: str) -> str:
return f"{base}/{name}" if base else name