feat: add file attachment support to issue tracker with Gitea mirroring
Implement full attachment CRUD for issues and comments, restricted to open issues only. Attachments are stored locally and mirrored to the Gitea tracker via new `mirror_attachment_to_gitea` and `set_gitea_asset_id` functions. Add `gitea_asset_id` column to the attachments table, extend `IssueForm` and `IssueCommentForm` with `attachment_uids` field, and expose new API endpoints for listing, adding, and deleting issue attachments. Update agent configuration to include web research tools, and document the new capability in README, AGENTS.md, and CLAUDE.md.
This commit is contained in:
@@ -313,6 +313,7 @@ def store_attachment(file_bytes, original_filename, user_uid):
|
||||
"image_height": image_height,
|
||||
"has_thumbnail": 1 if thumbnail else 0,
|
||||
"thumbnail_name": thumbnail,
|
||||
"gitea_asset_id": None,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
@@ -451,6 +452,69 @@ def link_attachments(uids, target_type, target_uid):
|
||||
)
|
||||
|
||||
|
||||
def set_gitea_asset_id(uid, asset_id):
|
||||
get_table("attachments").update(
|
||||
{"uid": uid, "gitea_asset_id": int(asset_id)}, ["uid"]
|
||||
)
|
||||
|
||||
|
||||
async def mirror_attachment_to_gitea(uid):
|
||||
from devplacepy.services.gitea import runtime
|
||||
from devplacepy.services.gitea.client import GiteaError
|
||||
|
||||
row = get_table("attachments").find_one(uid=uid, deleted_at=None)
|
||||
if not row:
|
||||
return None
|
||||
target_type = row.get("target_type", "")
|
||||
target_uid = row.get("target_uid", "")
|
||||
if target_type not in ("issue", "issue_comment") or not target_uid:
|
||||
return None
|
||||
path = ATTACHMENTS_DIR / row.get("directory", "") / row.get("stored_name", "")
|
||||
try:
|
||||
data = path.read_bytes()
|
||||
except OSError as exc:
|
||||
logger.warning("Cannot read attachment %s for Gitea mirror: %s", uid, exc)
|
||||
return None
|
||||
filename = row.get("original_filename") or row.get("stored_name") or "file"
|
||||
mime = row.get("mime_type") or "application/octet-stream"
|
||||
client = runtime.get_client()
|
||||
try:
|
||||
if target_type == "issue":
|
||||
asset = await client.create_issue_asset(
|
||||
int(target_uid), filename, data, mime
|
||||
)
|
||||
else:
|
||||
asset = await client.create_comment_asset(
|
||||
int(target_uid), filename, data, mime
|
||||
)
|
||||
except (GiteaError, ValueError) as exc:
|
||||
logger.warning("Gitea asset mirror failed for %s: %s", uid, exc)
|
||||
return None
|
||||
asset_id = int(asset.get("id", 0) or 0)
|
||||
if asset_id:
|
||||
set_gitea_asset_id(uid, asset_id)
|
||||
return asset_id
|
||||
|
||||
|
||||
async def remove_gitea_asset(row):
|
||||
from devplacepy.services.gitea import runtime
|
||||
from devplacepy.services.gitea.client import GiteaError
|
||||
|
||||
asset_id = int(row.get("gitea_asset_id") or 0)
|
||||
target_type = row.get("target_type", "")
|
||||
target_uid = row.get("target_uid", "")
|
||||
if not asset_id or not target_uid:
|
||||
return
|
||||
client = runtime.get_client()
|
||||
try:
|
||||
if target_type == "issue":
|
||||
await client.delete_issue_asset(int(target_uid), asset_id)
|
||||
elif target_type == "issue_comment":
|
||||
await client.delete_comment_asset(int(target_uid), asset_id)
|
||||
except (GiteaError, ValueError) as exc:
|
||||
logger.warning("Gitea asset delete failed for %s: %s", row.get("uid"), exc)
|
||||
|
||||
|
||||
def _unlink_attachment_files(row):
|
||||
stored_name = row.get("stored_name", "")
|
||||
directory = row.get("directory", "")
|
||||
@@ -618,6 +682,8 @@ def _row_to_attachment(row):
|
||||
"is_audio": row.get("mime_type", "").startswith("audio/"),
|
||||
"target_type": row.get("target_type", ""),
|
||||
"target_uid": row.get("target_uid", ""),
|
||||
"user_uid": row.get("user_uid", ""),
|
||||
"gitea_asset_id": row.get("gitea_asset_id") or None,
|
||||
"created_at": row.get("created_at", ""),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user