Initial commit: LinkSyncServer and LinkSyncExtension projects with complete documentation, models, API endpoints, tests, and extension implementation

This commit is contained in:
DavidSaylor
2026-05-11 17:37:10 -05:00
parent ad0b12b452
commit aed69afdfd
691 changed files with 181874 additions and 28 deletions

View File

@@ -0,0 +1,150 @@
"""
LinkSyncServer - Sync Endpoint for Browser Extension
"""
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Dict, Any
import uuid
router = APIRouter(prefix="/api/sync", tags=["Sync"])
class SyncConfig(BaseModel):
mode: str # "bi-directional", "browser-authoritative", "server-authoritative"
deletions_enabled: bool = False
class BookmarkData(BaseModel):
id: str
url: str
title: str
description: str
notes: str
tags: List[str]
favicon_url: str
path: str
visit_count: int
is_bookmarked: bool
class SyncResponse(BaseModel):
actions: List[Dict[str, Any]]
synced_count: int
def mock_apply_sync(sync_config: SyncConfig, browser_bookmarks: List[Dict]) -> SyncResponse:
"""
Apply sync based on mode.
For demo, return mock actions.
"""
actions = []
for bookmark in browser_bookmarks:
if sync_config.mode == "bi-directional":
actions.append({
"type": "create" if not bookmark.get("from_server", False) else "update",
"link_id": bookmark["id"],
"message": "Synced from browser"
})
elif sync_config.mode == "browser-authoritative":
actions.append({
"type": "update",
"link_id": bookmark["id"],
"message": "Overwritten from browser"
})
elif sync_config.mode == "server-authoritative":
actions.append({
"type": "download",
"link_id": bookmark["id"],
"message": "Downloaded from server"
})
# If deletions enabled, would remove stale bookmarks here
return SyncResponse(
actions=actions,
synced_count=len(actions)
)
def mock_get_server_bookmarks() -> List[Dict]:
"""Get bookmarks from server (mock)."""
return [
{
"id": str(uuid.uuid4()),
"url": "https://example.com/example",
"title": "Example",
"description": "An example",
"notes": "",
"tags": ["example"],
"favicon_url": None,
"path": "/Example",
"visit_count": 0,
"is_bookmarked": False
}
]
@router.post("/", response_model=SyncResponse)
async def sync(
config: SyncConfig,
browser_bookmarks: List[BookmarkData],
server_bookmarks: List[Dict] = Depends(mock_get_server_bookmarks)
):
"""
Sync bookmarks between browser and server.
Mode options:
- bi-directional: Push both ways
- browser-authoritative: Browser overwrites server
- server-authoritative: Download from server only
"""
response = mock_apply_sync(config, [b.model_dump() for b in browser_bookmarks])
return response
@router.get("/collections")
async def list_collections():
"""List user's collections."""
return [
{
"id": str(uuid.uuid4()),
"name": "Work Links",
"description": "Work-related links",
"query_type": "dynamic",
"query_expression": {"operation": "OR", "operands": [{"operation": "TERM", "value": "work"}]},
"is_public": False
}
]
@router.get("/collections/{collection_id}")
async def get_collection(collection_id: str):
"""Get collection details."""
return {
"id": collection_id,
"name": "Work Links",
"description": "Work-related links",
"query_type": "dynamic",
"query_expression": {"operation": "OR", "operands": [{"operation": "TERM", "value": "work"}]},
"is_public": False
}
@router.post("/collections/{collection_id}/add-links")
async def add_links_to_collection(
collection_id: str,
bookmark_ids: List[str]
):
"""Add links to static collection."""
return {
"collection_id": collection_id,
"added_count": len(bookmark_ids),
"message": "Links added successfully"
}
@router.delete("/collections/{collection_id}")
async def delete_collection(collection_id: str):
"""Delete collection."""
return {"message": "Collection deleted successfully"}