-
Notifications
You must be signed in to change notification settings - Fork 11
Tree node listing helpers + server-authoritative S3 tagging #403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ErykKul
wants to merge
32
commits into
develop
Choose a base branch
from
feature/configurable-uploads
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
65acf6a
feat: add configurable file upload options and related tests
ErykKul 5c78d15
Merge branch 'develop' into feature/configurable-uploads
ErykKul 3c2aaf8
Remove FilesConfig/useS3Tagging; drive tagging from server response
ErykKul 4fbcffe
Polish configurable upload PR
ErykKul c60e2b7
Trim upload PR scope
ErykKul 8c1e158
typo fix
ErykKul 0dccf99
Isolate set default template functional test
ErykKul f29ad91
Revert "Isolate set default template functional test"
ErykKul 07bf082
Scope format and lint scripts to ./src
ErykKul 571d625
Re-export DataverseApiAuthMechanism from public core surface
ErykKul 397d662
Add tree node listing SDK helpers (#6691)
ErykKul 0df68ec
test: follow IQSS/dataverse#12182 storage-driver endpoint move
ErykKul 17822cb
Document tree-node use cases and DataverseApiAuthMechanism re-export
ErykKul bb19183
Re-export DirectUploadClientConfig from public files index
ErykKul cccbc5a
Restore lint/format coverage for tests
ErykKul 8773e82
Document DirectUploadClient constructor break and tagging change in C…
ErykKul 460f36d
Document the tree endpoint Cache-Control as private, immutable to mat…
ErykKul 21550f8
Document older-server backwards-compat behavior for the tagging field
ErykKul 3d6f638
Default x-amz-tagging to dv-state=temp when the server omits the field
ErykKul 2e25eea
Drop unrelated lint/format scope drive-by from the branch
ErykKul 3c1fa22
Carry counts.bytes through tree response transformers
ErykKul 61058f6
Carry per-access folder counts through tree response transformers
ErykKul cf1723f
docs: tree node size/checksum reflect the original form under origina…
ErykKul 98ee8bd
Merge remote-tracking branch 'origin/develop' into feature/configurab…
ErykKul c727efd
Sync tree contract: retentionExpired access state, no-cache ETag, ser…
ErykKul 0fb753a
Address self-review: export DirectUploadClient, honor legacy numeric …
ErykKul f11ee66
Merge remote-tracking branch 'origin/develop' into feature/configurab…
ErykKul bef8107
Merge origin/develop: union tree-listing additions with exportDataset…
ErykKul 5c1247f
Enhance transformItem with type checks and error handling
ErykKul 292a587
tree node iteration condition fix and removed comments
ErykKul a008f51
Tree node and s3 no tagging integration tests
ErykKul 1f6dba6
Compare featured item HTML with a normalizer instead of an exact serv…
ErykKul File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| export { ReadError } from './domain/repositories/ReadError' | ||
| export { WriteError } from './domain/repositories/WriteError' | ||
| export { ApiConfig } from './infra/repositories/ApiConfig' | ||
| export { ApiConfig, DataverseApiAuthMechanism } from './infra/repositories/ApiConfig' | ||
| export { DvObjectOwnerNode, DvObjectType } from './domain/models/DvObjectOwnerNode' | ||
| export { PublicationStatus } from './domain/models/PublicationStatus' | ||
| export { StorageDriver } from './domain/models/StorageDriver' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| export enum FileTreeNodeType { | ||
| FOLDER = 'folder', | ||
| FILE = 'file' | ||
| } | ||
|
|
||
| export interface FileTreeFolderNode { | ||
| type: FileTreeNodeType.FOLDER | ||
| name: string | ||
| path: string | ||
| counts?: { | ||
| /** Total files anywhere in the folder's subtree (recursive). */ | ||
| files: number | ||
| /** Immediate subfolders only. */ | ||
| folders: number | ||
| /** | ||
| * Total size of all files in the subtree, in bytes. Uses the | ||
| * default-form file size — for ingested tabular files that's the | ||
| * converted TSV, not the original. Intended as a UX hint | ||
| * ("downloading this folder = N GB"); not authoritative under | ||
| * `originals=true`. | ||
| */ | ||
| bytes: number | ||
| /** | ||
| * Files in the subtree marked restricted. Mirrors the per-file | ||
| * `access` resolution — the buckets are mutually exclusive: a | ||
| * retention-expired file counts in `retentionExpired` even if it is | ||
| * also restricted, and a restricted file counts here even if it | ||
| * also carries an embargo. | ||
| */ | ||
| restricted: number | ||
| /** | ||
| * Non-restricted files in the subtree whose embargo has not yet | ||
| * lapsed. The "public" count is implied: `files - restricted - | ||
| * embargoed - retentionExpired`. | ||
| */ | ||
| embargoed: number | ||
| /** | ||
| * Files in the subtree whose retention period has expired — they | ||
| * cannot be served at all, so this state wins over `restricted` | ||
| * and `embargoed` in the per-file `access` resolution. | ||
| */ | ||
| retentionExpired: number | ||
| } | ||
| } | ||
|
|
||
| export interface FileTreeFileNode { | ||
| type: FileTreeNodeType.FILE | ||
| id: number | ||
| name: string | ||
| path: string | ||
| /** | ||
| * File size in bytes. For ingested tabular files this is the | ||
| * served-form (converted TSV) size by default; when the listing was | ||
| * requested with `originals=true` it is the saved original's size, | ||
| * matching the bytes `downloadUrl` then serves. | ||
| */ | ||
| size: number | ||
| contentType?: string | ||
| /** | ||
| * Resolved in this order: `retentionExpired` (the file cannot be | ||
| * served at all), then `restricted` (even when an embargo is also | ||
| * active), then `embargoed`, then `public`. The embargo/retention | ||
| * date checks match Dataverse's download enforcement: a file whose | ||
| * embargo lifts today reports `public` and is downloadable. | ||
| */ | ||
| access?: 'public' | 'restricted' | 'embargoed' | 'retentionExpired' | ||
| checksum?: { | ||
| type: string | ||
| value: string | ||
| } | ||
| downloadUrl: string | ||
| } | ||
|
|
||
| export type FileTreeNode = FileTreeFolderNode | FileTreeFileNode | ||
|
|
||
| export const isFileTreeFolderNode = (node: FileTreeNode): node is FileTreeFolderNode => | ||
| node.type === FileTreeNodeType.FOLDER | ||
|
|
||
| export const isFileTreeFileNode = (node: FileTreeNode): node is FileTreeFileNode => | ||
| node.type === FileTreeNodeType.FILE |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { FileTreeNode } from './FileTreeNode' | ||
|
|
||
| export enum FileTreeInclude { | ||
| ALL = 'all', | ||
| FOLDERS = 'folders', | ||
| FILES = 'files' | ||
| } | ||
|
|
||
| export enum FileTreeOrder { | ||
| NAME_AZ = 'NameAZ', | ||
| NAME_ZA = 'NameZA' | ||
| } | ||
|
|
||
| export interface FileTreePage { | ||
| path: string | ||
| items: FileTreeNode[] | ||
| nextCursor: string | null | ||
| limit: number | ||
| order: FileTreeOrder | ||
| include: FileTreeInclude | ||
| /** | ||
| * Total folders + files at the requested path. Snapshotted on the | ||
| * first page of a pagination walk and carried unchanged through the | ||
| * cursor for the rest of the walk — on draft versions, files added | ||
| * mid-walk are not reflected until a new walk starts. | ||
| */ | ||
| approximateCount?: number | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { IDatasetsRepository, ListDatasetTreeNodeParams } from '../repositories/IDatasetsRepository' | ||
| import { FileTreeNode } from '../models/FileTreeNode' | ||
|
|
||
| /** | ||
| * Async generator that exhaustively iterates the immediate children of the | ||
| * given path inside a dataset version, transparently following the | ||
| * `nextCursor` chain. | ||
| * | ||
| * Use this when you need every direct child of a folder; it does NOT recurse | ||
| * into subfolders — that is the caller's responsibility (e.g. pre-download | ||
| * enumeration walks the tree by re-invoking this iterator with each folder | ||
| * path it discovers). | ||
| */ | ||
| export class IterateDatasetTreeNode { | ||
| constructor(private readonly datasetsRepository: IDatasetsRepository) {} | ||
|
|
||
| async *execute(params: ListDatasetTreeNodeParams): AsyncGenerator<FileTreeNode> { | ||
| let cursor = params.cursor | ||
| do { | ||
| const page = await this.datasetsRepository.listDatasetTreeNode({ | ||
| ...params, | ||
| cursor | ||
| }) | ||
| for (const item of page.items) { | ||
| yield item | ||
| } | ||
| const nextCursor = page.nextCursor ?? undefined | ||
| // A keyset cursor must always advance. A server (or intermediary | ||
| // cache) echoing the cursor we just sent would otherwise loop this | ||
| // generator forever, re-fetching and re-yielding the same page. | ||
| if (nextCursor !== undefined && nextCursor === cursor) { | ||
| throw new Error( | ||
| `Dataset tree pagination cursor did not advance ("${nextCursor}"); aborting iteration` | ||
| ) | ||
| } | ||
| cursor = nextCursor | ||
| } while (cursor) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { UseCase } from '../../../core/domain/useCases/UseCase' | ||
| import { IDatasetsRepository, ListDatasetTreeNodeParams } from '../repositories/IDatasetsRepository' | ||
| import { FileTreePage } from '../models/FileTreePage' | ||
|
|
||
| export class ListDatasetTreeNode implements UseCase<FileTreePage> { | ||
| constructor(private readonly datasetsRepository: IDatasetsRepository) {} | ||
|
|
||
| /** | ||
| * Lists the immediate children of the given folder path inside a dataset | ||
| * version, returning a single page of folders and files. | ||
| * | ||
| * Folders are returned first, then files. Both are sorted by name. Use the | ||
| * returned `nextCursor` to keep paging the same folder. The cursor is | ||
| * opaque to callers and is server-validated; an invalid cursor yields a 400 | ||
| * from the API. | ||
| */ | ||
| async execute(params: ListDatasetTreeNodeParams): Promise<FileTreePage> { | ||
| return this.datasetsRepository.listDatasetTreeNode(params) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this check
cursor !== undefinedexplicitly instead of relying on truthinesswhile (cursor)?because nextCursor's type(string | null)permits'', andpage.nextCursor ?? undefinedonly substitutes fornull/undefined, not''?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch, thanks! Fix applied.