show directory metadata in archive browser
This commit is contained in:
@@ -953,35 +953,94 @@ def get_archive_tree(path: Optional[str] = None, db_session: Session = Depends(g
|
||||
|
||||
@router.get("/metadata", response_model=ItemMetadataSchema)
|
||||
def get_archive_item_metadata(path: str, db_session: Session = Depends(get_db)):
|
||||
"""Retrieves full version history and location details for an indexed file."""
|
||||
"""Retrieves full version history and location details for an indexed file or directory."""
|
||||
item = (
|
||||
db_session.query(models.FilesystemState)
|
||||
.filter(models.FilesystemState.file_path == path)
|
||||
.first()
|
||||
)
|
||||
if not item:
|
||||
|
||||
if item:
|
||||
# Exact file match
|
||||
versions = []
|
||||
for v in item.versions:
|
||||
versions.append(
|
||||
{
|
||||
"media_id": v.media.identifier,
|
||||
"media_type": v.media.media_type,
|
||||
"archive_id": v.file_number,
|
||||
"created_at": v.created_at,
|
||||
"is_split": v.is_split,
|
||||
"offset": v.offset_start,
|
||||
}
|
||||
)
|
||||
|
||||
return ItemMetadataSchema(
|
||||
id=item.id,
|
||||
path=item.file_path,
|
||||
type="file",
|
||||
size=item.size,
|
||||
mtime=datetime.fromtimestamp(item.mtime, tz=timezone.utc),
|
||||
last_seen_timestamp=item.last_seen_timestamp,
|
||||
sha256_hash=item.sha256_hash,
|
||||
is_ignored=item.is_ignored,
|
||||
versions=versions,
|
||||
)
|
||||
|
||||
# No exact match — check if this is a directory with archived children
|
||||
prefix = path if path.endswith("/") else path + "/"
|
||||
dir_stats = db_session.execute(
|
||||
text("""
|
||||
SELECT
|
||||
COUNT(*) as child_count,
|
||||
SUM(size) as total_size,
|
||||
MAX(mtime) as latest_mtime,
|
||||
MAX(last_seen_timestamp) as latest_seen
|
||||
FROM filesystem_state
|
||||
WHERE file_path LIKE :prefix
|
||||
"""),
|
||||
{"prefix": f"{prefix}%"},
|
||||
).fetchone()
|
||||
|
||||
if not dir_stats or dir_stats[0] == 0:
|
||||
raise HTTPException(status_code=404, detail="File not found in index.")
|
||||
|
||||
# Aggregate unique media locations for all children
|
||||
media_rows = db_session.execute(
|
||||
text("""
|
||||
SELECT DISTINCT
|
||||
sm.identifier as media_id,
|
||||
sm.media_type,
|
||||
MIN(fv.created_at) as earliest_created
|
||||
FROM file_versions fv
|
||||
JOIN storage_media sm ON sm.id = fv.media_id
|
||||
JOIN filesystem_state fs ON fs.id = fv.filesystem_state_id
|
||||
WHERE fs.file_path LIKE :prefix
|
||||
GROUP BY sm.identifier, sm.media_type
|
||||
"""),
|
||||
{"prefix": f"{prefix}%"},
|
||||
).fetchall()
|
||||
|
||||
versions = []
|
||||
for v in item.versions:
|
||||
for row in media_rows:
|
||||
versions.append(
|
||||
{
|
||||
"media_id": v.media.identifier,
|
||||
"media_type": v.media.media_type,
|
||||
"archive_id": v.file_number,
|
||||
"created_at": v.created_at,
|
||||
"is_split": v.is_split,
|
||||
"offset": v.offset_start,
|
||||
"media_id": row[0],
|
||||
"media_type": row[1],
|
||||
"archive_id": "—",
|
||||
"created_at": row[2],
|
||||
"is_split": False,
|
||||
"offset": 0,
|
||||
}
|
||||
)
|
||||
|
||||
return ItemMetadataSchema(
|
||||
id=item.id,
|
||||
path=item.file_path,
|
||||
size=item.size,
|
||||
mtime=datetime.fromtimestamp(item.mtime, tz=timezone.utc),
|
||||
last_seen_timestamp=item.last_seen_timestamp,
|
||||
sha256_hash=item.sha256_hash,
|
||||
is_ignored=item.is_ignored,
|
||||
id=-1,
|
||||
path=path,
|
||||
type="directory",
|
||||
size=dir_stats[1] or 0,
|
||||
mtime=datetime.fromtimestamp(dir_stats[2] or 0, tz=timezone.utc),
|
||||
last_seen_timestamp=dir_stats[3],
|
||||
child_count=dir_stats[0],
|
||||
versions=versions,
|
||||
)
|
||||
|
||||
@@ -1291,9 +1291,21 @@ def _resolve_ids_from_action(
|
||||
if action.ids:
|
||||
return action.ids
|
||||
if action.path_prefix:
|
||||
prefix = action.path_prefix
|
||||
if not prefix.endswith("/"):
|
||||
# If there are files under this path in the index, treat it as a directory
|
||||
has_children = (
|
||||
db_session.query(models.FilesystemState)
|
||||
.filter(models.FilesystemState.file_path.startswith(prefix + "/"))
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
if has_children:
|
||||
prefix += "/"
|
||||
|
||||
records = (
|
||||
db_session.query(models.FilesystemState)
|
||||
.filter(models.FilesystemState.file_path.startswith(action.path_prefix))
|
||||
.filter(models.FilesystemState.file_path.startswith(prefix))
|
||||
.all()
|
||||
)
|
||||
return [r.id for r in records]
|
||||
|
||||
+3
-1
@@ -30,7 +30,9 @@ app = FastAPI(
|
||||
|
||||
# Configure Cross-Origin Resource Sharing (CORS)
|
||||
# Use TAPEHOARD_CORS_ORIGINS env var (comma-separated) in production
|
||||
cors_origins = os.getenv("TAPEHOARD_CORS_ORIGINS", "*").split(",")
|
||||
# Default allows all origins (*), but explicitly add localhost:5174 for Playwright tests
|
||||
cors_default = "*,http://localhost:5174,http://localhost:5173"
|
||||
cors_origins = os.getenv("TAPEHOARD_CORS_ORIGINS", cors_default).split(",")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[o.strip() for o in cors_origins if o.strip()],
|
||||
|
||||
@@ -36,7 +36,7 @@ export default defineConfig({
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: [
|
||||
{
|
||||
command: 'cd ../backend && rm -f e2e_test.db* && DATABASE_URL="sqlite:///e2e_test.db" TAPEHOARD_TEST_MODE="true" uv run python -m app.start_test_server --host 0.0.0.0 --port 8001',
|
||||
command: 'cd ../backend && rm -f e2e_test.db* && DATABASE_URL="sqlite:///e2e_test.db" TAPEHOARD_TEST_MODE="true" TAPEHOARD_CORS_ORIGINS="*,http://localhost:5174" uv run python -m app.start_test_server --host 0.0.0.0 --port 8001',
|
||||
url: 'http://localhost:8001/health',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
|
||||
@@ -121,7 +121,9 @@ test.describe('TapeHoard Golden Path', () => {
|
||||
console.log('Step 7: Verify Protection');
|
||||
await page.goto('/index-browser');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByText(SOURCE_ROOT).first().dblclick();
|
||||
// Click on the item in the file list (not the tree)
|
||||
// Tree items have role="treeitem", file list rows have role="button"
|
||||
await page.getByRole('button', { name: SOURCE_ROOT }).first().dblclick();
|
||||
await page.getByText('subfolder').first().dblclick();
|
||||
await expect(page.getByText('test_file_2.txt')).toBeVisible();
|
||||
await expect(page.getByText('TAPE001')).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user