Skip to content

Commit a219823

Browse files
authored
Merge pull request #3903 from OpenNeuroOrg/getFiles-refs-fix
refactor(server): Resolve refs for getFiles to prevent cache pollution/duplication
2 parents ae8683a + 2035dde commit a219823

4 files changed

Lines changed: 81 additions & 0 deletions

File tree

packages/openneuro-server/src/cache/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ export enum CacheType {
1616
brainInitiative = "brainInitiative",
1717
validation = "validation",
1818
dataciteYml = "dataciteYml",
19+
gitRef = "ref",
1920
}

packages/openneuro-server/src/datalad/files.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
setTree,
1616
type TreeEntry,
1717
} from "../cache/tree"
18+
import CacheItem, { CacheType } from "../cache/item"
1819
import { join } from "node:path"
20+
import { captureException } from "@sentry/node"
1921

2022
/**
2123
* Convert to URL compatible path
@@ -271,6 +273,37 @@ async function cacheWorkerTrees(
271273
return result
272274
}
273275

276+
/**
277+
* Resolve a git reference (branch, tag, or short commit hash) to a full commit hash.
278+
* @param datasetId The dataset ID.
279+
* @param treeish The git reference to resolve.
280+
* @returns The full commit hash.
281+
*/
282+
export const resolveGitRef = async (
283+
datasetId: string,
284+
treeish: string,
285+
): Promise<string> => {
286+
const cache = new CacheItem(redis, CacheType.gitRef, [datasetId, treeish])
287+
return cache.get(async () => {
288+
const url = `http://${
289+
getDatasetWorker(datasetId)
290+
}/datasets/${datasetId}/refs/${treeish}`
291+
const response = await fetch(url)
292+
if (!response.ok) {
293+
throw new Error(
294+
`Failed to resolve git reference ${treeish}: ${response.statusText}`,
295+
)
296+
}
297+
const data = await response.json()
298+
if (!data.hash) {
299+
throw new Error(
300+
`Invalid response from datalad worker for git reference ${treeish}`,
301+
)
302+
}
303+
return data.hash
304+
})
305+
}
306+
274307
/**
275308
* Get files for a specific revision (tree hash or commit hash).
276309
* Uses content-addressed caching keyed by full git hash.
@@ -279,6 +312,17 @@ export const getFiles = async (
279312
datasetId: string,
280313
treeish: string,
281314
): Promise<DatasetFile[]> => {
315+
// Guard against requests without a full git hash (40 = SHA-1, 64 = SHA-256)
316+
if (treeish.length !== 40 && treeish.length !== 64) {
317+
try {
318+
treeish = await resolveGitRef(datasetId, treeish)
319+
} catch (error) {
320+
captureException(error, {
321+
tags: { datasetId, treeish, source: "resolveGitRef" },
322+
})
323+
throw new Error(`Invalid git reference: ${treeish}`)
324+
}
325+
}
282326
// Try cache first
283327
const cached = await getTree(redis, treeish)
284328
if (cached) {

services/datalad/datalad_service/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from datalad_service.handlers.remote_import import RemoteImportResource
3636
from datalad_service.handlers.info import InfoResource
3737
from datalad_service.handlers.fsck import FsckResource
38+
from datalad_service.handlers.refs import RefsResource
3839
from datalad_service.middleware.auth import AuthenticateMiddleware
3940
from datalad_service.middleware.error import CustomErrorHandlerMiddleware
4041

@@ -118,6 +119,7 @@ def create_app():
118119
dataset_remote_import_resource = RemoteImportResource(store)
119120
dataset_info_resource = InfoResource(store)
120121
dataset_fsck_resource = FsckResource(store)
122+
dataset_refs_resource = RefsResource(store)
121123

122124
app.add_route('/heartbeat', heartbeat)
123125

@@ -181,6 +183,7 @@ def create_app():
181183
app.add_route(
182184
'/datasets/{dataset:dataset}/import/{import_id}', dataset_remote_import_resource
183185
)
186+
app.add_route('/datasets/{dataset:dataset}/refs/{treeish}', dataset_refs_resource)
184187

185188
if 'pytest' in sys.modules:
186189
return app # Do not wrap in Sentry middleware during tests
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import falcon
2+
import pygit2
3+
import os
4+
5+
6+
class RefsResource:
7+
"""
8+
Resolve a git reference (branch, tag, or short commit hash) to a full commit hash.
9+
"""
10+
11+
def __init__(self, store):
12+
self.store = store
13+
14+
async def on_get(self, req, resp, dataset, treeish):
15+
dataset_path = self.store.get_dataset_path(dataset)
16+
if not os.path.exists(dataset_path):
17+
resp.media = {'error': f'Dataset "{dataset}" not found.'}
18+
resp.status = falcon.HTTP_NOT_FOUND
19+
return
20+
21+
try:
22+
repo = pygit2.Repository(dataset_path)
23+
commit = repo.revparse_single(treeish)
24+
resp.media = {'hash': str(commit.id)}
25+
resp.status = falcon.HTTP_OK
26+
except pygit2.GitError:
27+
resp.media = {
28+
'error': f'Git reference "{treeish}" not found or is ambiguous in dataset "{dataset}".'
29+
}
30+
resp.status = falcon.HTTP_NOT_FOUND
31+
except Exception as e:
32+
resp.media = {'error': f'An unexpected error occurred: {e}'}
33+
resp.status = falcon.HTTP_INTERNAL_SERVER_ERROR

0 commit comments

Comments
 (0)