From 31723c0841f079006f602d235ea208b96755efcb Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Mon, 14 Apr 2025 11:20:49 -0700 Subject: [PATCH 01/19] initial draft initial draft Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitignore | 2 + docs/README-comparison.md | 66 +++++ docs/README-epic.md | 163 ++++++++++++ docs/README-git-sync.md | 363 ++++++++++++++++++++++++++ docs/README-gitlfs-meta.md | 355 +++++++++++++++++++++++++ docs/README-gitlfs-remote-buckets.md | 247 ++++++++++++++++++ docs/README-gitlfs.md | 164 ++++++++++++ docs/README-release-test.md | 154 +++++++++++ docs/README.md | 22 ++ docs/images/github-sync-flowchart.png | Bin 0 -> 67895 bytes 10 files changed, 1536 insertions(+) create mode 100644 .gitignore create mode 100644 docs/README-comparison.md create mode 100644 docs/README-epic.md create mode 100644 docs/README-git-sync.md create mode 100644 docs/README-gitlfs-meta.md create mode 100644 docs/README-gitlfs-remote-buckets.md create mode 100644 docs/README-gitlfs.md create mode 100644 docs/README-release-test.md create mode 100644 docs/README.md create mode 100644 docs/images/github-sync-flowchart.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f32e31a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +.DS_Store diff --git a/docs/README-comparison.md b/docs/README-comparison.md new file mode 100644 index 0000000..2686a80 --- /dev/null +++ b/docs/README-comparison.md @@ -0,0 +1,66 @@ +# Comparison: Git LFS and g3t Integrated Data Platform (ACED-IDP) +A comparative overview of two distinct approaches to managing and storing large project data files: Git Large File Storage (Git LFS) and the ACED Integrated Data Platform (ACED-IDP). + +--- + +## Git Large File Storage (Git LFS) + +**Purpose:** Git LFS is an open-source Git extension designed to handle large files efficiently within Git repositories. + +**Key Features:** + +- **Pointer-Based Storage:** Replaces large files (e.g., audio, video, datasets) in the Git repository with lightweight text pointers, while storing the actual file contents on a remote server. + +- **Seamless Git Integration:** Allows developers to use standard Git commands (`add`, `commit`, `push`, `pull`) without altering their workflow. + +- **Selective File Tracking:** Developers specify which file types to track using `.gitattributes`, enabling granular control over large file management. + +- **Storage Efficiency:** By offloading large files, it keeps the Git repository size manageable, improving performance for cloning and fetching operations. + +**Use Cases:** + +- Software development projects involving large binary assets, such as game development, multimedia applications, or data science projects. + +--- + +## ACED Integrated Data Platform (ACED-IDP) + +**Purpose:** ACED-IDP is a specialized data commons platform developed by the International Alliance for Cancer Early Detection (ACED) to facilitate secure and structured sharing of research data among member institutions. + +**Key Features:** + +- **Gen3-Based Infrastructure:** Utilizes Gen3, an open-source data commons framework, to manage data submission, storage, and access. + +- **Command-Line Interface (CLI):** Provides the `gen3-tracker (g3t)` CLI tool for researchers to create projects, upload files, and associate metadata incrementally. + +- **FHIR Metadata Integration:** Supports the addition of Fast Healthcare Interoperability Resources (FHIR) metadata, enhancing data interoperability and standardization. + +- **Role-Based Access Control:** Implements fine-grained access controls to ensure data security and compliance with privacy regulations. + +- **Data Exploration and Querying:** Offers tools for data exploration and querying, facilitating collaborative research and analysis. +**Use Cases:** + +- Biomedical research projects requiring secure, standardized, and collaborative data management, particularly in multi-institutional settings. + +--- + +## Comparative Summary + +| Feature | Git LFS | ACED-IDP | +|---------------------------|--------------------------------------------------------|-----------------------------------------------------------| +| **Primary Use Case** | Managing large files in software development projects | Collaborative biomedical research data management | +| **Integration** | Seamless with Git workflows | Built on Gen3 framework with specialized CLI tools | +| **Data Storage** | Remote storage with Git pointers | Structured data commons with metadata support | +| **Access Control** | Inherits Git repository permissions | Role-based access control for data security | +| **Metadata Support** | Limited | Comprehensive, including FHIR standards | +| **Collaboration Features**| Standard Git collaboration tools | Enhanced tools for data exploration and querying | + +--- + +**Conclusion:** + +- **Git LFS** is ideal for developers seeking to manage large files within their existing Git workflows, offering a straightforward solution without the need for additional infrastructure. + +- **ACED-IDP** caters to the complex needs of collaborative biomedical research, providing a robust platform for secure data sharing, standardized metadata integration, and advanced data exploration capabilities. + +The choice between Git LFS and ACED-IDP depends on the specific requirements of the project, including the nature of the data, collaboration needs, and compliance considerations. diff --git a/docs/README-epic.md b/docs/README-epic.md new file mode 100644 index 0000000..68aad17 --- /dev/null +++ b/docs/README-epic.md @@ -0,0 +1,163 @@ + + +# πŸš€ Epic: Develop `git-gen3` Tool for Git-Based Gen3 Integration + +> Create a Git-native utility to track and synchronize remote object metadata, generate FHIR-compliant metadata, and manage Gen3 access control using `git-sync`. + +--- + +## 🧭 Sprint 0: Architecture Spike + +### 🎯 Goal: +De-risk implementation by validating core architectural assumptions and tool compatibility. + +### πŸ”¬ Tasks: +| ID | Task Description | Est. | +|--------|------------------------------------------------------------------------|------| +| SPK-1 | Prototype `track-remote` to fetch metadata (e.g., ETag, size) from S3/GCS | 1d | +| SPK-2 | Simulate `.lfs-meta/metadata.json` usage in Git repo + commit/push | 0.5d | +| SPK-3 | Test `init-meta` to produce `DocumentReference.ndjson` via `g3t`-style logic | 1d | +| SPK-4 | Validate `git-sync` role mappings and diffs against Gen3 fence API | 1d | +| SPK-5 | Evaluate GitHub template DX: hooks, portability, local usage | 0.5d | + +### βœ… Deliverables: +- Prototype CLI for `track-remote` +- Sample `.lfs-meta/metadata.json` and generated `META/DocumentReference.ndjson` +- Credential access matrix (S3, GCS, Azure) +- Feasibility report for Git-driven role syncing via `git-sync` +- Recommendation on proceeding with full implementation + +--- + +## 🧭 Sprint 1: CLI Bootstrapping & Remote File Tracking + +### 🎯 Goal: +Create the `git-gen3` CLI structure and implement the ability to track remote cloud objects in Git without downloading them. + +### πŸ”¨ Tasks: +| ID | Task Description | Est. | +|------|------------------------------------------------------|------| +| S1-1 | Scaffold `git-gen3` CLI with Click (Python) or Cobra (Go) | 2d | +| S1-2 | Implement `track` and `track-remote` subcommands | 2d | +| S1-3 | Write to `.lfs-meta/metadata.json` | 1d | +| S1-4 | Support auth with AWS, GCS, Azure (env vars + profiles) | 1d | +| S1-5 | Add `pre-push` hook to validate metadata before push | 1d | +| S1-6 | Unit tests for `track-remote` and metadata structure | 1d | + +### βœ… Deliverables: +- Functional CLI command: `git-gen3 track-remote s3://...` +- `.lfs-meta/metadata.json` updated and committed in Git +- Git hook active for metadata validation +- CI-ready foundation for next sprint + +--- + +## 🧭 Sprint 2: Metadata Initialization + FHIR Generation + +### 🎯 Goal: +Transform `.lfs-meta/metadata.json` entries into Gen3-compatible `DocumentReference.ndjson` metadata using FHIR structure. + +### πŸ”¨ Tasks: +| ID | Task Description | Est. | +|------|--------------------------------------------------------------------|------| +| S2-1 | Implement `init-meta` to emit `META/DocumentReference.ndjson` | 2d | +| S2-2 | Populate FHIR fields: `subject`, `context.related`, `attachment` | 1d | +| S2-3 | Create `validate-meta` command to check metadata completeness | 1d | +| S2-4 | Write tests for `init-meta` and FHIR formatting | 1d | +| S2-5 | Document schema, CLI usage, and FHIR integration points | 1d | + +### βœ… Deliverables: +- `git-gen3 init-meta` produces valid FHIR NDJSON +- Tool handles patient/specimen references +- Tests validate output conformance +- Documentation aligns with `g3t upload` workflows + +--- + +## 🧭 Sprint 3: Git-Sync Integration & Access Control + +### 🎯 Goal: +Replace `collaborator` and `project-management` with Git-based role assignments using `git-sync` and Gen3 fence APIs. + +### πŸ”¨ Tasks: +| ID | Task Description | Est. | +|------|-------------------------------------------------------------------|------| +| S3-1 | Integrate `git-sync` YAML/CSV parser into `git-gen3 sync-users` | 2d | +| S3-2 | Implement dry-run and apply modes for syncing to Gen3 fence | 1d | +| S3-3 | Add change auditing (diff viewer from Git commits) | 1d | +| S3-4 | End-to-end test: Git β†’ Gen3 user role propagation | 1d | +| S3-5 | Write user guide and governance documentation | 1d | + +### βœ… Deliverables: +- `git-gen3 sync-users` CLI reads Git-tracked access config +- Git diffs capture permission changes over time +- Gen3 access control (via Fence) is synced reliably +- Finalized documentation for institutional onboarding + +--- + +## πŸ“… Sprint Timeline Summary + +| Sprint | Focus | Duration | Deliverables | +|--------|----------------------------------|----------|-----------------------------------------------| +| 0 | Architecture validation (spike) | 1 week | Prototypes + greenlight for implementation | +| 1 | Remote file tracking | 2 weeks | `track-remote`, `.lfs-meta`, validation hooks | +| 2 | Metadata generation (FHIR) | 2 weeks | FHIR output, `init-meta`, validation tooling | +| 3 | Git-based access control | 2 weeks | `sync-users`, Git audit trail, Fence sync | + +--- + +## πŸ›  Toolchain + +| Purpose | Tool/Stack | +|------------------------|---------------------------| +| CLI Language | Python (Click) or Go (Cobra) | +| Object Store APIs | boto3 (S3), gcsfs, Azure SDK | +| Metadata Serialization | JSON, FHIR NDJSON | +| Access Sync | git-sync + Gen3 Fence | +| Testing | `pytest` or `go test` | +| Docs | Markdown, GitHub Pages | + +--- + +## 🧭 Sprint 4: User Testing, Documentation, and Release Planning + +### 🎯 Goal: +Conduct functional and usability testing, finalize user documentation, and prepare for internal/external release of the `git-gen3` tool. + +--- + +### πŸ”¨ Tasks: +| ID | Task Description | Est. | +|------|------------------------------------------------------------------------------|------| +| S4-1 | Recruit early adopters from internal teams or pilot projects | 0.5d | +| S4-2 | Collect and triage feedback via GitHub issues or survey | 1d | +| S4-3 | Perform functional validation of all workflows (track, init-meta, sync) | 1d | +| S4-4 | Finalize and polish all CLI command help strings and usage messages | 0.5d | +| S4-5 | Write end-user guide (markdown or GitHub Pages) with examples and FAQs | 1d | +| S4-6 | Create changelog and release notes for v1.0 | 0.5d | +| S4-7 | Define release checklist and governance process (e.g., approval flow) | 0.5d | +| S4-8 | Tag first release, publish GitHub release, optionally register PyPI/Homebrew| 0.5d | + +--- + +### βœ… Deliverables: +- End-user documentation published and linked from the repo +- Feedback collected from test users and incorporated as GitHub issues +- Final `v1.0.0` tag and release notes +- Optional: Package published to PyPI (Python) or Homebrew (Go binary) + +--- + +### πŸ“… Sprint Timeline Summary (Updated) + +| Sprint | Focus | Duration | Deliverables | +|--------|----------------------------------|----------|-----------------------------------------------| +| 0 | Architecture validation (spike) | 1 week | Prototypes + greenlight for implementation | +| 1 | Remote file tracking | 2 weeks | `track-remote`, `.lfs-meta`, validation hooks | +| 2 | Metadata generation (FHIR) | 2 weeks | FHIR output, `init-meta`, validation tooling | +| 3 | Git-based access control | 2 weeks | `sync-users`, Git audit trail, Fence sync | +| 4 | Testing, docs, release planning | 1 week | Docs, feedback, `v1.0.0` release | + + +--- diff --git a/docs/README-git-sync.md b/docs/README-git-sync.md new file mode 100644 index 0000000..fc23690 --- /dev/null +++ b/docs/README-git-sync.md @@ -0,0 +1,363 @@ +# Overview `git-sync` + +Contents: + +* A **high-level architecture for the `git-sync` project**, where **GitHub,etc. becomes the source of truth (system of record)** for project roles, replacing Synapse. The target system remains **Gen3**, where roles and access need to be synchronized. +* Support for **GitLab** or **other Git servers**, we introduce an **abstraction layer** for the identity and role retrieval logic. This layer separates the **source system** (GitHub, GitLab, etc.) from the **target system** (Gen3), making the architecture **extensible and pluggable**. +* **Unit and integration test specifications** for the `git-sync` project with pluggable Git-based role sources and Gen3 as the target system. + +--- + +## 🎯 Goal + +> **Sync project role assignments from GitHub teams and collaborators to Gen3's access control system**. + +--- + +## 🧭 Conceptual Overview + +```text + +---------------------+ + | GitHub Org | <-- system of record + | - Teams | + | - Collaborators | + +---------+-----------+ + | + | REST API + v + +---------------------+ +---------------------+ + | git-sync CLI +------>+ Gen3 Access API | + | - Fetch & map roles | | - Projects & ACLs | + | - Transform to Gen3 | | - Policies | + +---------------------+ +---------------------+ +``` + +--- + +## 🧱 Architectural Components + +### 1. **GitHub as the System of Record** + +- **Source of role info**: + - [Organization Teams](https://docs.github.com/en/organizations/collaborating-with-groups-in-your-organization/about-teams) + - Represent Gen3 "project roles" (e.g., `project-admins`, `project-members`) + - [Repository Collaborators](https://docs.github.com/en/rest/collaborators/collaborators?apiVersion=2022-11-28) + - Fallback or direct per-project access info + +- **API Use**: + - List team members + - Map team slugs to Gen3 projects/roles + - Fetch repo collaborators and permission levels (`pull`, `push`, `admin`) + +--- + +### 2. **git-sync CLI Service** + +A command-line tool/service that: + +- Authenticates with GitHub and Gen3 +- Loads configuration mapping GitHub entities to Gen3 projects and roles +- Periodically or on-demand performs synchronization + +#### Functions: + +| Function | Description | +|-----------------------------|-------------| +| `fetch_github_teams()` | Get org teams, members, and slugs | +| `map_to_gen3_roles()` | Transform GitHub teams β†’ Gen3 roles | +| `fetch_repo_collaborators()`| Identify individual users & access | +| `sync_to_gen3()` | Write roles to Gen3 using `fence` API | + +--- + +### 3. **Configuration Layer** + +Example YAML mapping: + +```yaml +projects: + project-xyz: + github_teams: + - aced-project-xyz-admins: project-admin + - aced-project-xyz-members: project-member + repos: + - aced/project-xyz +``` + +--- + +### 4. **Target System: Gen3 (Fence Authz)** + +- **API Integration**: + - Use Gen3/fence endpoint: `PUT /user` or `PATCH /access` for roles +- **Mapped Roles**: + - `project-admin` + - `project-member` + - `data-submitter` + +--- + +## πŸ”’ Security & Auth + +- **GitHub Auth**: + - Use GitHub App or PAT with `read:org` and `repo` scopes +- **Gen3 Auth**: + - Use API key or JWT token authorized to manage users + +--- + +## πŸ“‹ Sync Flow + +![](images/github-sync-flowchart.png) +--- + +## πŸš€ Future Enhancements + +- Bi-directional diff reporting (GitHub vs Gen3) +- Dry-run and audit modes +- GitHub webhooks for near-real-time sync +- GitHub Actions-based CI for automation +- Slack/email alerts for sync failures + +--- + + +## 🧱 High-Level Architecture with Abstraction Layer +To enable support for **GitLab** or **other Git servers**, we introduce an **abstraction layer** for the identity and role retrieval logic. This layer separates the **source system** (GitHub, GitLab, etc.) from the **target system** (Gen3), making the architecture **extensible and pluggable**. + + +```text + +---------------------+ +---------------------+ +----------------------+ + | GitHub / GitLab | | Bitbucket etc. | | Other Identity | + | (source plugins) | | (optional source) | | Providers | + +----------+----------+ +----------+----------+ +----------+-----------+ + | | | + | | | + +------------+---------------+------------------------------+ + | + v + +-----------------------------------------------+ + | RoleSourceAdapter Interface (Abstract) | + | - get_users_for_project(project_id) | + | - get_teams_for_project(project_id) | + | - get_user_roles() | + +-----------------------------------------------+ + | + +---------------+-----------------+ + | | + +-------v--------+ +--------v--------+ + | GitHubAdapter | | GitLabAdapter | ← add more: BitbucketAdapter, etc. + +----------------+ +-----------------+ + + | + v + +--------------------------------------+ + | git-sync Core Logic | + | - Loads config & adapter | + | - Maps source roles to Gen3 roles | + | - Pushes to Gen3 via Gen3 API | + +--------------------------------------+ + | + v + +------------------------+ + | Gen3 API (Fence) | + +------------------------+ +``` + +--- + +## 🧩 Interface Definition: `RoleSourceAdapter` + +```python +from typing import List, Dict + +class RoleSourceAdapter: + def get_users_for_project(self, project_id: str) -> List[str]: + raise NotImplementedError + + def get_teams_for_project(self, project_id: str) -> Dict[str, List[str]]: + raise NotImplementedError + + def get_user_roles(self) -> Dict[str, str]: + """Optional: direct user-to-role mapping""" + raise NotImplementedError +``` + +--- + +## πŸ”Œ GitHub Adapter Example + +```python +class GitHubAdapter(RoleSourceAdapter): + def __init__(self, github_token: str, org: str): + self.token = github_token + self.org = org + + def get_teams_for_project(self, project_id): + # use GitHub REST API to get team memberships + return { + "aced-project-xyz-admins": ["alice", "bob"], + "aced-project-xyz-members": ["carol", "dave"] + } + + def get_user_roles(self): + # flatten and map to Gen3 roles + return { + "alice": "project-admin", + "carol": "project-member" + } +``` + +--- + +## πŸ”Œ GitLab Adapter Example (Stub) + +```python +class GitLabAdapter(RoleSourceAdapter): + def __init__(self, token: str, group_id: str): + self.token = token + self.group_id = group_id + + def get_teams_for_project(self, project_id): + # use GitLab group and project member APIs + pass + + def get_user_roles(self): + # similar mapping logic + pass +``` + +--- + +## πŸ›  CLI Usage Example + +```bash +# In config.yaml +source: + type: github + org: aced + token_env: GITHUB_TOKEN + +# Could also be: +# type: gitlab +# group: aced-projects +# token_env: GITLAB_TOKEN +``` + +The CLI would dynamically load the adapter based on the `type`. + +--- + +## βœ… Benefits of This Design + +| Benefit | Description | +|--------------------------|-------------| +| πŸ”Œ **Extensible** | Easily add new providers (GitHub, GitLab, Bitbucket, etc.) | +| πŸ”„ **Pluggable** | Source logic swappable without changing Gen3 sync logic | +| πŸ”’ **Encapsulated Auth** | Each adapter handles its own tokens/API nuances | +| πŸ§ͺ **Testable** | Adapters are unit-testable in isolation | + +--- + +## βœ… **Unit Test Specifications** +**Unit and integration test specifications** for the `git-sync` project with pluggable Git-based role sources and Gen3 as the target system. + +### πŸ”§ 1. **Adapter Interface Compliance** +Test that all adapter classes correctly implement the `RoleSourceAdapter` interface. + +- `test_github_adapter_implements_interface()` +- `test_gitlab_adapter_implements_interface()` + +### πŸ§ͺ 2. **GitHubAdapter** +- Mock GitHub API responses using `requests-mock` or `unittest.mock` +- Tests: + - `test_get_teams_for_project_returns_expected_structure()` + - `test_get_user_roles_maps_correct_roles()` + - `test_handles_empty_team_membership_gracefully()` + +### πŸ§ͺ 3. **GitLabAdapter** +- Stubbed or mocked API interactions +- Tests: + - `test_get_users_from_gitlab_group()` + - `test_role_mapping_with_gitlab_permissions()` + +### πŸ§ͺ 4. **Core Logic** +- Tests for transformation and mapping: + - `test_map_team_to_gen3_role()` + - `test_generate_patch_payload_for_gen3()` + - `test_detect_added_and_removed_users()` + +### πŸ§ͺ 5. **Config Loader** +- Validate schema and default fallbacks: + - `test_config_validates_required_fields()` + - `test_loads_correct_adapter_from_type()` + +### πŸ§ͺ 6. **CLI Command** +Use `click.testing.CliRunner`: +- `test_sync_command_runs_with_github_config()` +- `test_invalid_config_returns_error()` + +--- + +## πŸ” **Integration Test Specifications** + +### 🌐 1. **Mock GitHub + Gen3** +Set up mocks or test containers for GitHub API and Gen3 Fence. + +- Use `responses` or `httpx_mock` for mocking GitHub/Gen3 endpoints +- Tests: + - `test_full_sync_applies_roles_to_gen3()` + - `test_users_removed_from_github_are_removed_from_gen3()` + - `test_users_added_to_team_are_reflected_in_gen3()` + +### πŸ” 2. **End-to-End Sync Flow** +- Fixture: + - `config.yaml` + - Mocked GitHub API for team memberships + - Mocked Gen3 `/user` and `/access` endpoints +- Validate: + - API calls are made + - Correct role updates are sent + - Logging and reporting capture expected output + +### πŸ”’ 3. **Security Edge Cases** +- Token expiration +- Invalid org/repo +- Permissions mismatch +- Tests: + - `test_invalid_github_token_raises_error()` + - `test_gen3_rejects_unauthorized_user_changes()` + +--- + +## πŸ“ Suggested Test Directory Structure + +``` +tests/ +β”œβ”€β”€ unit/ +β”‚ β”œβ”€β”€ test_github_adapter.py +β”‚ β”œβ”€β”€ test_gitlab_adapter.py +β”‚ β”œβ”€β”€ test_core_logic.py +β”‚ └── test_config_loader.py +β”œβ”€β”€ integration/ +β”‚ β”œβ”€β”€ test_end_to_end_github_sync.py +β”‚ β”œβ”€β”€ test_error_handling.py +β”‚ └── test_gen3_interactions.py +└── fixtures/ + └── github_team_response.json +``` + +--- + +## βœ… Tools & Fixtures + +| Tool | Purpose | +|-----------------|-----------------------------------| +| `pytest` | Core testing framework | +| `responses` | HTTP mocking for REST APIs | +| `click.testing` | CLI command testing | +| `pydantic` | Config schema validation | +| `tox` or `nox` | Multi-environment test automation | + +--- + diff --git a/docs/README-gitlfs-meta.md b/docs/README-gitlfs-meta.md new file mode 100644 index 0000000..be4b3be --- /dev/null +++ b/docs/README-gitlfs-meta.md @@ -0,0 +1,355 @@ + +--- + +## 🧩 Overview + +**Goal**: Enable this usage: + +```bash +git add path/to/file --patient Patient/1234 --specimen Specimen/ABC567 +``` + +…and have the `--patient` and `--specimen` metadata passed through to your **Git LFS custom transfer agent**, such as [`lfs-s3`](https://github.com/nicolas-graves/lfs-s3), for use in metadata handling or cloud-side tagging. + +--- + +## ❗Problem + +Git **does not support arbitrary flags on `git add`**. + +**Solution**: Use **Git LFS pre-push hooks and custom transfer metadata** to attach additional metadata. + +--- + +### 1. **Capture Extra Metadata Outside `git add`** + +Since we can't modify `git add`: + +- Track extra metadata in a sidecar file (e.g., `.lfs-meta`) +- Use an extended command like: + + ```bash + git lfs-meta track path/to/file --patient Patient/1234 --specimen Specimen/ABC567 + ``` + +That command would append this to `.lfs-meta.json`: + +```json +{ + "path/to/file": { + "patient": "Patient/1234", + "specimen": "Specimen/ABC567" + } +} +``` + +--- + +### 2. **Enhance the Git LFS Transfer Agent** + +> Optional: Adding S3 tags or other metadata to the object in S3. + +Git LFS passes information to your custom transfer agent (like `lfs-s3`) using stdin/stdout JSON messages. + +You can modify `lfs-s3` to: + +- Parse the filename it's transferring +- Look up `patient`/`specimen` metadata from `.lfs-meta.json` +- Push that metadata to S3 (e.g., as object tags or upload metadata) + +πŸ”§ **Example agent snippet (Go, inside `lfs-s3`)**: + +```go +meta := readMetaFile(".lfs-meta.json") +filePath := filepath.Base(obj.Path) + +if info, ok := meta[filePath]; ok { + s3Client.PutObjectTagging(&s3.PutObjectTaggingInput{ + Bucket: aws.String(bucket), + Key: aws.String(obj.Oid), + Tagging: &s3.Tagging{ + TagSet: []*s3.Tag{ + {Key: aws.String("Patient"), Value: aws.String(info.Patient)}, + {Key: aws.String("Specimen"), Value: aws.String(info.Specimen)}, + }, + }, + }) +} +``` + +--- + +## πŸ“„ Example Workflow + +```bash +git lfs track "*.bin" +git add foo.bin + +# Add metadata via a companion command +git lfs-meta track foo.bin --patient Patient/001 --specimen Specimen/XYZ + +git commit -m "Add patient-associated data" +git push +``` + +--- + +## πŸ›  Implementation Notes + +| Component | Description | +|------------------|-------------| +| `.lfs-meta.json` | Project-local map of file path β†’ metadata | +| `git lfs-meta` | New CLI wrapper to manage sidecar file | +| `lfs-s3` | Enhanced to load `.lfs-meta.json` and inject metadata during upload | + +--- + +## βœ… Advantages + +- No change to Git core or Git LFS binary +- Clean separation of metadata via `.lfs-meta.json` +- Reuses standard Git + LFS behavior +- Fully compatible with custom transfer agents + +--- + +## βš™οΈ Configuration: `lfs-meta` Git Integration + +### πŸ“¦ 1. Install the `lfs-meta` Tool + +Install globally or per-project. Example (Python-based): + +```bash +pip install git-lfs-meta +# or +go install github.com/username/repository@latest +``` + +Ensure it's in your `$PATH`: + +```bash +which lfs-meta +``` + +--- + +### πŸ—‚οΈ 2. Create `.lfs-meta/metadata.json` + +In your Git repo: + +```bash +mkdir -p .lfs-meta +touch .lfs-meta/metadata.json +``` + +Track this in your repo: + +```bash +echo ".lfs-meta/metadata.json" >> .gitignore +``` + +Optionally, use `.lfs-meta/.metaignore` to exclude paths from metadata. + +--- + +### 🧩 3. Add `lfs-meta` as a Git subcommand + +You can use Git's alias feature: + +```bash +git config alias.lfs-meta '!lfs-meta' +``` + +Now you can run: + +```bash +git lfs-meta track path/to/file --patient Patient/1234 +``` + +--- + +### πŸͺ 4. Configure a Git LFS Pre-Push Hook (Optional) + +To automatically sync metadata during push, create: + +`.git/hooks/pre-push` + +```bash +#!/bin/bash +# Hook to prepare metadata for LFS transfer agent + +if [ -f ".lfs-meta/metadata.json" ]; then + echo "[lfs-meta] Metadata file detected." +else + echo "[lfs-meta] No metadata file present." +fi +``` + +Make it executable: + +```bash +chmod +x .git/hooks/pre-push +``` + +For more advanced use, this hook could: +- Validate `.lfs-meta/metadata.json` +- Ensure required fields are set before push + +--- + +### πŸ›  5. Custom Git Config (optional) + +To keep Git aware of `lfs-meta` behavior, configure: + +```bash +git config --local lfs.meta.enabled true +git config --local lfs.meta.path .lfs-meta/metadata.json +``` + +Read these with: + +```bash +git config --get lfs.meta.path +``` + +--- + +## 🧬 Section: FHIR Metadata Initialization via `lfs-meta` +Extends the `lfs-meta` command to **initialize FHIR metadata** (in the style of `g3t meta init` from the [`gen3_util`](https://github.com/ACED-IDP/gen3_util) project), by reading a sidecar metadata file (like `.lfs-meta/metadata.json`) and generating a `META/DocumentReference.ndjson` file. + +### 🎯 Goal + +Add a command to `lfs-meta`: + +```bash +lfs-meta init-meta +``` + +This reads `.lfs-meta/metadata.json` and generates a valid FHIR `DocumentReference, Patient, ... ` ndjson file in `META/`. + +--- + +### πŸ“‚ Input: `.lfs-meta/metadata.json` + +```json +{ + "foo.vcf": { + "patient": "Patient/1234", + "specimen": "Specimen/XYZ" + }, + "bar.pdf": { + "patient": "Patient/5678" + } +} +``` + +--- + +### πŸ“„ Output: `META/DocumentReference.ndjson, META/Patient.ndjson, ...` + +```json +{"resourceType":"DocumentReference","content":[{"attachment":{"url":"s3://bucket/foo.vcf","title":"foo.vcf"}}],"subject":{"reference":"Patient/1234"},"context":{"related":[{"reference":"Specimen/XYZ"}]}} +{"resourceType":"DocumentReference","content":[{"attachment":{"url":"s3://bucket/bar.pdf","title":"bar.pdf"}}],"subject":{"reference":"Patient/5678"}} +``` + + +--- + +### πŸ§ͺ CLI Integration + +Add to `lfs-meta` as a subcommand: + +```bash +lfs-meta init-meta +``` + +Options: +- `--output`: Where to write the `.ndjson` +- `--bucket`: Base URI for constructing FHIR `attachment.url` + +--- + +### πŸ“ Directory Structure After Init + +``` +. +β”œβ”€β”€ .lfs-meta/ +β”‚ └── metadata.json +β”œβ”€β”€ META/ +β”‚ └── DocumentReference.ndjson, etc. +``` + +--- + +### βœ… Benefits + +| Feature | Value | +|----------------------------------|--------------------------------------| +| πŸ” Integrates with Gen3 Uploads | Compatible with `gen3_util` metadata flow | +| 🧬 FHIR-compliant | Proper `DocumentReference` structure | +| πŸ“¦ Reusable | Automates metadata for downstream sync tools | + +--- + + +## βœ… Resulting Workflow + +```bash +git lfs track "*.vcf" +git add foo.vcf + +# Associate metadata (via configured alias) +git lfs-meta track foo.vcf --patient Patient/123 --specimen Specimen/ABC + +git commit -m "Added foo.vcf with metadata" +git push +``` + +Absolutely β€” here’s a **test specification section** for the `lfs-meta` feature that initializes FHIR metadata from a sidecar file, compatible with `gen3_util`. + +--- + +## βœ… Section: Unit and Integration Tests for `lfs-meta init-meta` + +--- + +### πŸ§ͺ Unit Test Specifications + +| Test Name | Description | +|---------------------------------------------|-------------| +| `test_generate_single_documentreference()` | Generates a valid FHIR `DocumentReference` for one file | +| `test_generate_with_patient_only()` | Handles entries that include only the patient reference | +| `test_generate_with_patient_and_specimen()` | Handles entries with both patient and specimen | +| `test_missing_metadata_fields()` | Gracefully skips or warns on invalid metadata (e.g. missing file or fields) | +| `test_output_is_valid_ndjson()` | Validates that output is newline-delimited JSON objects | +| `test_bucket_override()` | Ensures custom S3 base path is respected | +| `test_empty_metadata()` | Outputs nothing (or warns) if input metadata is empty | + + +--- + +### πŸ” Integration Test Specifications + +| Scenario | Setup | Expected Behavior | +|-----------------------------------------|-------------------------------------------|-------------------| +| `test_lfs_meta_end_to_end_minimal()` | .lfs-meta/metadata.json β†’ `init-meta` | Produces `META/DocumentReference.ndjson` | +| `test_meta_used_in_gen3_upload()` | Full Git repo, push to Gen3 | Metadata is accepted by Gen3 API or `g3t upload` | +| `test_multiple_files_ndjson_format()` | Multiple entries in sidecar | Multiple NDJSON lines generated | +| `test_script_idempotency()` | Run `init-meta` twice | Output is consistent and append-safe | +| `test_no_metadata_file()` | No `.lfs-meta/metadata.json` | Graceful failure or warning message | + + +--- + +### πŸ“ Suggested Test Structure + +``` +tests/ +β”œβ”€β”€ unit/ +β”‚ └── test_meta_generation.py +β”œβ”€β”€ integration/ +β”‚ └── test_cli_init_meta.py +└── fixtures/ + └── sample_metadata.json +``` + diff --git a/docs/README-gitlfs-remote-buckets.md b/docs/README-gitlfs-remote-buckets.md new file mode 100644 index 0000000..4b22e0b --- /dev/null +++ b/docs/README-gitlfs-remote-buckets.md @@ -0,0 +1,247 @@ +Git LFS (Large File Storage) **does not natively handle files stored in cloud buckets (like S3, GCS, Azure Blob)** unless additional tools or integrations are introduced. + +--- + +## 🧠 What Git LFS Does + +Git LFS is designed to: + +- Replace large files in a Git repo with lightweight pointers. +- Store the actual file content on **a Git LFS server**, which is usually: + - Co-located with Git hosting (e.g., GitHub, GitLab, Bitbucket), or + - Hosted separately (e.g., custom LFS servers). + +When you `git clone` or `git pull`, the Git LFS client fetches the file content from the LFS server. + +--- + +## 🚫 Limitations with Cloud Buckets + +Git LFS: + +- **Does not automatically recognize or manage files** stored in external cloud buckets like S3, GCS, or Azure. +- **Cannot directly sync or link to files in a cloud bucket** without: + - Downloading them manually and committing them via LFS. + - Using custom tooling to bridge Git LFS pointers with cloud bucket contents. + +--- + +## βœ… Possible Workarounds or Integrations + +To incorporate cloud bucket storage with Git LFS, users may: + +### 1. **Use a custom Git LFS server that backs to S3** +- Example: [lfs-test-server](https://github.com/git-lfs/lfs-test-server) or other S3-backed LFS servers. +- Stores LFS objects in S3, **but still requires Git LFS client operations** for tracking/pulling. + +### 2. **Manually sync cloud bucket contents with Git LFS** +- Download from the cloud bucket. +- Use `git lfs track` to commit and push to Git LFS server. +- Duplication risk: The file now exists in both Git LFS and the cloud bucket. + +### 3. **Symlink or metadata approaches (not portable)** +- Use `.gitattributes` to track LFS pointer. +- Maintain cloud object metadata (e.g., S3 URI) in the repo. +- Requires external scripts or hooks to resolve and download actual content. + +--- + +## βœ… Summary Table + +| Feature | Git LFS Support | +|--------------------------------------|------------------| +| Track large files in Git | βœ… Yes | +| Native cloud bucket integration (S3, GCS) | ❌ No | +| Support for S3-backed custom LFS servers | βœ… With setup | +| Automatically fetch from cloud buckets | ❌ No | +| Point LFS to a cloud bucket URI | ❌ No native support | + +--- + +## 🧩 Missing Feature: Index Remote Files without Download/Upload + +To support tracking **remote URLs** (e.g., S3, GCS, etc.) **without downloading or uploading files**, and storing that information in `.lfs-meta/metadata.json`, you can extend the current design as follows: + +--- + + +### βœ… Motivation +You want to track and index remote files (e.g., in object stores) using Git + LFS-like semantics, but **without actually downloading or uploading files**. Instead, file metadata is logged, version-controlled, and made available for later workflows like validation or FHIR metadata generation. + +--- + +## πŸ”„ Updated `.lfs-meta/metadata.json` Format + +Now includes a `remote_url` key, replacing the need to add the actual file content to the repo: + +```json +{ + "data/foo.vcf": { + "remote_url": "s3://my-bucket/data/foo.vcf", + "etag": "abc123etag", + "size": 12345678, + "patient": "Patient/1234", + "specimen": "Specimen/XYZ" + } +} +``` + +This file is tracked in Git, but the referenced file is **never downloaded or uploaded**. + +--- + +## πŸš€ Updated Usage Workflow + +### Step 1: Track a Remote File +```bash +lfs-meta track-remote s3://my-bucket/data/foo.vcf \ + --path data/foo.vcf \ + --patient Patient/1234 \ + --specimen Specimen/XYZ +``` + +This command: +- Writes to `.lfs-meta/metadata.json` +- Extracts or validates remote file size, ETag, etc. via cloud APIs + +### Step 2: Skip `git add data/foo.vcf` β€” no file is present + +Instead, only the metadata is committed: + +```bash +git add .lfs-meta/metadata.json +git commit -m "Track remote file foo.vcf without downloading" +``` + +--- + +## βš™οΈ Updates to `README.md` + +You should update the **Usage Workflow** and **metadata.json example** in the README to include: + +### πŸ“¦ Track Remote File +```bash +lfs-meta track-remote s3://my-bucket/data/foo.vcf \ + --path data/foo.vcf \ + --patient Patient/1234 +``` + +### πŸ“ Sample `.lfs-meta/metadata.json` +```json +{ + "data/foo.vcf": { + "remote_url": "s3://my-bucket/data/foo.vcf", + "etag": "abc123etag", + "size": 12345678, + "patient": "Patient/1234" + } +} +``` + +### 🧬 Generate FHIR Metadata +```bash +lfs-meta init-meta \ + --input .lfs-meta/metadata.json \ + --output META/DocumentReference.ndjson +``` + +Generates: +```json +{ + "resourceType": "DocumentReference", + "content": [ + { + "attachment": { + "url": "s3://my-bucket/data/foo.vcf", + "title": "foo.vcf" + } + } + ], + "subject": { + "reference": "Patient/1234" + } +} +``` + +--- + +## βœ… Benefits + +| Feature | Description | +|--------------------------|-------------| +| ☁️ Remote index only | Tracks remote data without local storage | +| πŸ“‹ Auditable metadata | Commit metadata to Git without binary bloat | +| πŸ”„ Interoperable with Gen3| Downstream tools can consume this | +| πŸ” Permissions respected | No direct copy of sensitive files | + +--- + +# Credential management `track-remote` command obtains the credentials it needs to read metadata from remote object stores (e.g., S3, GCS, Azure Blob). + +--- + +## πŸ” Credential Handling for `track-remote` + +The `lfs-meta track-remote` command must authenticate to the cloud provider in order to retrieve metadata such as file size, ETag, or content type. This is done **without downloading the file**, using read-only **head/object metadata** APIs. + +Supported cloud providers (initial targets): +- βœ… AWS S3 +- βœ… Google Cloud Storage (GCS) +- βœ… Azure Blob Storage + +### πŸ”‘ Credential Lookup Strategy + +The command uses the following order of precedence to locate credentials: + +--- + +### πŸ“¦ AWS S3 + +| Method | Description | +|----------------------------|-------------| +| `AWS_PROFILE` | Use a named profile from `~/.aws/credentials` | +| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | Set directly as environment variables | +| EC2/ECS/IRSA IAM Roles | Automatically used in cloud environments with role-based access | + +> πŸ’‘ You can simulate this locally: +```bash +export AWS_PROFILE=aced-research +lfs-meta track-remote s3://my-bucket/data/foo.vcf --path data/foo.vcf +``` + +--- + +### 🌍 Google Cloud Storage (GCS) + +| Method | Description | +|------------------------------|-------------| +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to a service account key JSON | +| gcloud CLI default credentials | Automatically picked up if `gcloud auth application-default login` is used | + +> πŸ’‘ Example: +```bash +export GOOGLE_APPLICATION_CREDENTIALS=~/gcs-access-key.json +lfs-meta track-remote gs://my-bucket/data/foo.vcf --path data/foo.vcf +``` + +--- + +### ☁️ Azure Blob Storage + +| Method | Description | +|------------------------------|-------------| +| `AZURE_STORAGE_CONNECTION_STRING` | Full connection string for access | +| `AZURE_STORAGE_ACCOUNT` + `AZURE_STORAGE_KEY` | Account name and key variables | +| Azure CLI login | Supports `az login` if the SDK allows fallback | + +--- + +### πŸ”§ Fallback + +If credentials are not detected automatically, `lfs-meta track-remote` should: +- Display a clear error message +- Suggest how to set environment variables +- Optionally support a `--credentials` flag for custom paths or credential profiles + +--- + diff --git a/docs/README-gitlfs.md b/docs/README-gitlfs.md new file mode 100644 index 0000000..cdb8249 --- /dev/null +++ b/docs/README-gitlfs.md @@ -0,0 +1,164 @@ +# git-lfs-meta-template + +> **Template Repository** +> +> This is a GitHub **template repository**. Click **"Use this template"** on GitHub to bootstrap a new project with Git LFS support, metadata tracking, and FHIR integration. + +A Git project archetype for managing large files with Git LFS + S3 and synchronizing metadata with FHIR DocumentReferences, supporting integration with Gen3 via `g3t`. Tool-agnostic: the `lfs-meta` utility can be written in **Python**, **Go**, or any language that conforms to expected input/output behavior. + +--- + +## 🌐 Project Layout + +```bash +git-lfs-meta-template/ +β”œβ”€β”€ .gitignore +β”œβ”€β”€ .gitattributes +β”œβ”€β”€ .lfs-meta/ +β”‚ └── metadata.json +β”œβ”€β”€ META/ +β”‚ └── DocumentReference.ndjson +β”‚ └── Patient.ndjson, etc... +β”œβ”€β”€ hooks/ +β”‚ └── pre-push +β”œβ”€β”€ lfs_meta/ # Optional directory for your implementation +β”‚ └── ... +β”œβ”€β”€ tests/ +β”‚ β”œβ”€β”€ unit/ +β”‚ └── integration/ +β”œβ”€β”€ requirements.txt # If Python is used +β”œβ”€β”€ go.mod # If Go is used +└── README.md +``` + +--- + +## πŸš€ Getting Started + +### 1. Use This Template on GitHub + +1. Go to the repository page on GitHub. +2. Click the green **"Use this template"** button. +3. Create a new repository from this template. + +Alternatively, clone the project manually: + +```bash +git clone https://github.com/YOUR_ORG/git-lfs-meta-template.git +cd git-lfs-meta-template +``` + +### 2. Install the `lfs-meta` Tool +Install the `lfs-meta` tool in your preferred language: + +#### Python +```bash +pip install -e . # assumes setup.py or pyproject.toml exists +``` + +#### Go +```bash +go build -o lfs-meta ./cmd/lfs-meta +mv lfs-meta /usr/local/bin/ +``` + +Ensure it's available on your `$PATH`: +```bash +which lfs-meta +``` + +--- + +## βš–οΈ Git LFS Setup +```bash +git lfs install +git lfs track "*.bin" +echo "*.bin filter=lfs diff=lfs merge=lfs -text" >> .gitattributes +``` + +--- + +## βš™οΈ Configure Git Hooks + +Enable the `pre-push` hook: +```bash +chmod +x hooks/pre-push +ln -s ../../hooks/pre-push .git/hooks/pre-push +``` + +This will validate `.lfs-meta/metadata.json` before push. + +--- + +## ⚑ Usage Workflow + +### Add a Large File +```bash +git add data/foo.vcf +git commit -m "Add file" +``` + +### Track Metadata +```bash +lfs-meta track data/foo.vcf --patient Patient/1234 --specimen Specimen/XYZ +``` + +### Generate FHIR Metadata +```bash +lfs-meta init-meta --output META/DocumentReference.ndjson --bucket s3://my-bucket +``` + +--- + +## βœ… Tests + +Run all tests: +```bash +pytest tests/ # If Python is used +# or +go test ./... # If Go is used +``` + +--- + +## 🌿 Example .lfs-meta/metadata.json +```json +{ + "data/foo.vcf": { + "patient": "Patient/1234", + "specimen": "Specimen/XYZ" + } +} +``` + +--- + +## πŸ”Ή Pre-Push Hook +```bash +#!/bin/bash +# hooks/pre-push + +if [ ! -f ".lfs-meta/metadata.json" ]; then + echo "[lfs-meta] No metadata file found. Skipping." + exit 0 +fi + +lfs-meta validate --file .lfs-meta/metadata.json || { + echo "[lfs-meta] Metadata validation failed" + exit 1 +} +``` + +--- + +## πŸ† Credits +- Inspired by `g3t meta init` (ACED-IDP) +- Custom LFS support with S3 via [lfs-s3](https://github.com/nicolas-graves/lfs-s3) + +--- + +## ✨ Future Extensions +- Auto-tag S3 objects with metadata +- Metadata schema validation + +--- diff --git a/docs/README-release-test.md b/docs/README-release-test.md new file mode 100644 index 0000000..604338e --- /dev/null +++ b/docs/README-release-test.md @@ -0,0 +1,154 @@ +# πŸ§ͺ `lfs-meta` Pilot Test Script + +> Please follow the steps below to test core functionality of the `lfs-meta` tool. Report any issues to the project team via GitHub or the feedback form. + +--- + +## βœ… Prerequisites + +Before starting, ensure you have: + +- Access to a Git repo cloned from the `lfs-meta` template +- A working installation of the `lfs-meta` CLI +- Access to an S3, GCS, or Azure bucket (read-only) +- Python or Go runtime (depending on implementation) +- A `fence` endpoint (or staging Gen3 system) if testing user sync + +--- + +## 🧭 Part 0 – Track a Local File + +### 1.1 Track a local File + +```bash +git add data/test.vcf +``` + +βœ… Expected result: +- `.lfs-meta/metadata.json` is updated + + +--- + +## 🧭 Part 1 – Track a Remote File + +### 1.1 Track a Remote File + +```bash +lfs-meta track-remote s3://my-bucket/path/to/test.vcf \ + --path data/test.vcf \ + --patient Patient/1234 \ + --specimen Specimen/XYZ +``` + +βœ… Expected result: +- `.lfs-meta/metadata.json` is updated with `remote_url`, `size`, `etag`, etc. + +--- + +### 1.2 Commit the Metadata + +```bash +git add .lfs-meta/metadata.json +git commit -m "Track remote object test.vcf" +``` + +βœ… Expected result: +- Git diff shows new metadata +- No large file is downloaded or committed + +--- + +## 🧬 Part 2 – Generate FHIR Metadata + +### 2.1 Generate `DocumentReference.ndjson` + +```bash +lfs-meta init-meta \ + --input .lfs-meta/metadata.json \ + --output META/DocumentReference.ndjson \ + --bucket s3://my-bucket +``` + +βœ… Expected result: +- `META/DocumentReference.ndjson` is created +- File includes `Patient`, `Specimen`, and S3 URL as FHIR attachment + +--- + +### 2.2 Validate the Output + +```bash +lfs-meta validate-meta --file META/DocumentReference.ndjson +``` + +βœ… Expected result: +- β€œValidation passed” message (or warning if required fields are missing) + +--- + +## πŸ‘₯ Part 3 – Sync User Roles with Gen3 + +### 3.1 Create Access Config + +Create a YAML file at `.access.yaml`: + +```yaml +project_id: test-project +roles: + - username: alice@example.org + role: submitter + - username: bob@example.org + role: reader +``` + +βœ… Expected result: +- YAML is committed to Git and version-controlled + +--- + +### 3.2 Dry-Run the Sync + +```bash +lfs-meta sync-users --dry-run --input .access.yaml +``` + +βœ… Expected result: +- Diff is shown: who will be added/removed from Gen3 +- No changes are applied + +--- + +### 3.3 Apply the Sync (Optional) + +```bash +lfs-meta sync-users --input .access.yaml --confirm +``` + +βœ… Expected result: +- Users are updated in Gen3 +- Git commit acts as audit trail + +--- + +## πŸ“‹ Part 4 – Submit Feedback + +Please provide feedback on: + +- 🧠 Was the tool intuitive to use? +- 🧱 Did any commands fail or behave unexpectedly? +- πŸ“ Were the docs clear and complete? +- πŸ§ͺ Any bugs or unexpected behavior? + +➑ Submit GitHub Issues or fill out the pilot feedback form: +**[Feedback Form Link]** + +--- + +## πŸ’‘ Optional Tests + +- Try with GCS or Azure remote objects +- Test invalid metadata (missing patient/specimen) +- Clone the repo on another machine and repeat the workflow + +--- \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..48f5f3c --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +# Documentation Index + +Welcome to the `git-gen3` documentation! Below is an index of the available documentation files in this directory. + +--- + +## πŸ“š Documentation Files + +1. [README-comparison](README-comparison.md) + - A comparison of the `lfs-meta` tool with other tools and approaches. + - Discusses the advantages and disadvantages of each approach. + - Provides a summary of the key features and capabilities of `lfs-meta`. +2. [Architecture and Sprint Plan](README-epic.md) + - Overview of project goals, sprint breakdowns, and deliverables. +3. [Git LFS Metadata Tracking](README-gitlfs.md) + - Details on how to track large files using Git LFS +4. [Git LFS Metadata](README-gitlfs-meta.md) + - Information on how to manage metadata for large files in Git. +5. [Git LFS Remote Buckets](README-gitlfs-remote-buckets.md) + - Details for tracking remote files without downloading them. +6. [Release Testing](README-release-test.md) + - Guidelines for testing the release process and ensuring functionality. \ No newline at end of file diff --git a/docs/images/github-sync-flowchart.png b/docs/images/github-sync-flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..a72ac5c2436e603d0cff968ffa89bc28a1dc4b30 GIT binary patch literal 67895 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 V-|xZ7JqW7Eprc`+UZQFn^6#|{f%gCa literal 0 HcmV?d00001 From ce767090a34296eb61122a226e55497eabeb324d Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Wed, 16 Apr 2025 13:05:52 -0700 Subject: [PATCH 02/19] adds sequence diagram --- docs/README-epic.md | 20 ++++++++++-------- ...s.md => README-gitlfs-template-project.md} | 0 docs/README.md | 17 +++++++++------ docs/images/gen3-lfs.png | Bin 0 -> 114500 bytes 4 files changed, 22 insertions(+), 15 deletions(-) rename docs/{README-gitlfs.md => README-gitlfs-template-project.md} (100%) create mode 100644 docs/images/gen3-lfs.png diff --git a/docs/README-epic.md b/docs/README-epic.md index 68aad17..e1a9914 100644 --- a/docs/README-epic.md +++ b/docs/README-epic.md @@ -12,18 +12,20 @@ De-risk implementation by validating core architectural assumptions and tool compatibility. ### πŸ”¬ Tasks: -| ID | Task Description | Est. | -|--------|------------------------------------------------------------------------|------| -| SPK-1 | Prototype `track-remote` to fetch metadata (e.g., ETag, size) from S3/GCS | 1d | -| SPK-2 | Simulate `.lfs-meta/metadata.json` usage in Git repo + commit/push | 0.5d | -| SPK-3 | Test `init-meta` to produce `DocumentReference.ndjson` via `g3t`-style logic | 1d | -| SPK-4 | Validate `git-sync` role mappings and diffs against Gen3 fence API | 1d | -| SPK-5 | Evaluate GitHub template DX: hooks, portability, local usage | 0.5d | +| ID | Task Description | Est. | +|-------|------------------------------------------------------------------------------|------| +| SPK-1 | Prototype `track-remote` to fetch metadata (e.g., ETag, size) from S3/GCS | 1d | +| SPK-2 | Simulate `.lfs-meta/metadata.json` usage in Git repo + commit/push | 0.5d | +| SPK-3 | Test `init-meta` to produce `DocumentReference.ndjson` via `g3t`-style logic | 1d | +| SPK-4 | Validate `git-sync` role mappings and diffs against Gen3 fence API | 1d | +| SPK-5 | Evaluate GitHub template DX: hooks, portability, local usage | 0.5d | +| SPK-6 | Validate `auth-sync` deprecate client facing project management | 4d | +| SPK-7 | Validate `gen3-client` can UChicago's go code be installed and called? | 4d | ### βœ… Deliverables: -- Prototype CLI for `track-remote` +- Prototype CLI for `track-remote` - not currently part of got-lfs + - How are user credentials handled? - Sample `.lfs-meta/metadata.json` and generated `META/DocumentReference.ndjson` -- Credential access matrix (S3, GCS, Azure) - Feasibility report for Git-driven role syncing via `git-sync` - Recommendation on proceeding with full implementation diff --git a/docs/README-gitlfs.md b/docs/README-gitlfs-template-project.md similarity index 100% rename from docs/README-gitlfs.md rename to docs/README-gitlfs-template-project.md diff --git a/docs/README.md b/docs/README.md index 48f5f3c..0960ad1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,11 +12,16 @@ Welcome to the `git-gen3` documentation! Below is an index of the available docu - Provides a summary of the key features and capabilities of `lfs-meta`. 2. [Architecture and Sprint Plan](README-epic.md) - Overview of project goals, sprint breakdowns, and deliverables. -3. [Git LFS Metadata Tracking](README-gitlfs.md) - - Details on how to track large files using Git LFS -4. [Git LFS Metadata](README-gitlfs-meta.md) +3. [Auth-sync](README-git-sync.md) + - Details on how to sync authentication and authorization with github/synpase/etc as the system of record for project membership. +4. [Git LFS project archetype](README-gitlfs-template-project.md) + - Exemplar user project structure +5. [Git LFS Metadata](README-gitlfs-meta.md) - Information on how to manage metadata for large files in Git. -5. [Git LFS Remote Buckets](README-gitlfs-remote-buckets.md) +6. [Git LFS Remote Buckets](README-gitlfs-remote-buckets.md) - Details for tracking remote files without downloading them. -6. [Release Testing](README-release-test.md) - - Guidelines for testing the release process and ensuring functionality. \ No newline at end of file +7. [Release Testing](README-release-test.md) + - Guidelines for testing the release process and ensuring functionality. + +## Overview +![](images/gen3-lfs.png) \ No newline at end of file diff --git a/docs/images/gen3-lfs.png b/docs/images/gen3-lfs.png new file mode 100644 index 0000000000000000000000000000000000000000..5da3f5547f601ecad8340b04fae1005bbb7846ea GIT binary patch literal 114500 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 literal 0 HcmV?d00001 From 41c23c5b744402058365c638057ced6a2b90f296 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Wed, 16 Apr 2025 13:07:49 -0700 Subject: [PATCH 03/19] adds sequence diagram txt --- docs/images/gen3-lfs.txt | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/images/gen3-lfs.txt diff --git a/docs/images/gen3-lfs.txt b/docs/images/gen3-lfs.txt new file mode 100644 index 0000000..573ae6e --- /dev/null +++ b/docs/images/gen3-lfs.txt @@ -0,0 +1,76 @@ +title gen3-lfs +participant remote-bucket-or-filesystem +participant local-filesystem +actor user +participant git +participant gen3-lfs +participant git-sync +participant git-server +participant gen3 +participant gen3-managed-bucket +participant transfer-service +note right of transfer-service: globus, rsync + +alt project-init +user->git-server: create project, add collaborators +note right of git-sync: cron | web-hook +git-sync->git-server: harvest repo users and roles +git-sync->gen3: create project, add users +user->gen3: get credentials +end + +alt add-local +note right of user: upload local file to gen3-managed-bucket +user->git: add local-file +gen3-lfs->local-filesystem: read attributes +user->git: commit +git->gen3-lfs: create / update meta +user->git: push +git->gen3-lfs: hook: validate +git->gen3-lfs: "clean" +gen3-lfs->local-filesystem: read contents +gen3-lfs->gen3: index +gen3-lfs->gen3-managed-bucket: upload +end + +alt add-remote-index +note right of user: upload remote file to gen3 index only +user->git: add remote-file +gen3-lfs->remote-bucket-or-filesystem: read attributes +user->git: commit +git->gen3-lfs: create / update meta +user->git: push +git->gen3-lfs: hook: validate +git->gen3-lfs: "clean" +gen3-lfs->local-filesystem: read contents +gen3-lfs->gen3: index +end + +alt add-remote-content +note right of user: schedule remote file to gen3 managed bucket +user->git: add remote-file +gen3-lfs->remote-bucket-or-filesystem: read attributes +user->git: commit +git->gen3-lfs: create / update meta +user->git: push +git->gen3-lfs: hook: validate +git->gen3-lfs: "clean" +gen3-lfs->transfer-service: xfer request +gen3-lfs->gen3: index +transfer-service->remote-bucket-or-filesystem: xfer read +transfer-service->gen3-managed-bucket: xfer write +end + +alt read/download/pull +note right of user: git pull, website download +user->gen3: read index +alt index only +note right of user: if index only generate scp or bucket copy commands +end +alt gen3 managed +note right of user: download or pull +git->gen3-lfs: "smudge" +gen3-lfs->gen3-managed-bucket: read +gen3-lfs->local-filesystem: write +end +end \ No newline at end of file From 7e9b3c6b0aa1fe1aabad126d2ca6657381a71151 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Mon, 21 Apr 2025 08:50:22 -0700 Subject: [PATCH 04/19] adds overview --- docs/README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/docs/README.md b/docs/README.md index 0960ad1..e23f566 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,6 +3,81 @@ Welcome to the `git-gen3` documentation! Below is an index of the available documentation files in this directory. --- +## Overview + +Based on the current structure of the ACED Integrated Data Platform (IDP), which utilizes the `g3t` command-line tool for project creation, file uploads, and metadata association ξˆ€citeξˆ‚turn0search0, it's advisable to refactor this monolithic approach into modular utilities. This will enhance maintainability, scalability, and facilitate targeted enhancements. + +--- + +## 🧱 Proposed Modular Architecture +Transitioning to a modular architecture involves decomposing the monolith into distinct utilities, each responsible for a specific domain + +### 1. **Project Management Utility** + +**Responsibilities:** +-Initialize and manage project structures +-Handle user roles and permissions +-Integrate with git servers for audit trails project membership + + +**Implementation Suggestions:** +-Develop a CLI tool, e.g., `auth-sync`, to manage project lifecycles +-Utilize configuration files (YAML/JSON) to define project metadata +-Integrate with Git for version control and collaboration + +### 2. **File Transfer Utility** + +**Responsibilities:** +-Handle uploading and downloading of data files +-Support resumable transfers and integrity checks +-Manage storage backend interactions (e.g., S3, GCS) + +**Implementation Suggestions:** +-Create a tools, e.g., `git-lfs extentions, git add/pull url`, to abstract file operations +-Incorporate support for various storage backends using plugins or adapters +-Implement checksum verification to ensure data integrity + +### 3. **Metadata Management Utility** + +**Responsibilities:** +-Facilitate the creation, validation, and submission of metadata +-Transform metadata into required formats (e.g., FHIR resources) +-Ensure compliance with data standards and schemas + +**Implementation Suggestions:** +-Develop a utility, e.g., `git meta init/validate/etc`, to manage metadata workflows +-Leverage existing tools like `g3t_etl` for data transformation +-Incorporate schema validation to enforce data quality + +--- + +## πŸ”„ Integration Strategy + +To ensure seamless operation between these utilities: + +* Establish a shared configuration system to maintain consistency across tool. +* Provide comprehensive documentation and user guides to facilitate adoption. + +--- + +## πŸ› οΈ Implementation Roadmap + +1. **Assessment Phase:** + - Evaluate the current monolithic system to identify components for extraction. + - Prioritize functionalities based on user needs and system dependencies. + +2. **Development Phase:** + - Iteratively develop and test each utility. + - Ensure backward compatibility where necessary. + +3. **Deployment Phase:** + - Roll out utilities to a subset of users for feedback. + - Monitor performance and gather user input for refinements. + +4. **Documentation and Training:** + - Update documentation to reflect the new modular structure. + - Conduct training sessions to familiarize users with the new tools. + ## πŸ“š Documentation Files From 16e2082d18cdf915efbb3fd9d9650a31459ab941 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Wed, 23 Apr 2025 17:35:09 -0700 Subject: [PATCH 05/19] adds trackremote --- docs/README-gitlfs-meta.md | 4 +- docs/README-trackremote.md | 97 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 docs/README-trackremote.md diff --git a/docs/README-gitlfs-meta.md b/docs/README-gitlfs-meta.md index be4b3be..545fc3f 100644 --- a/docs/README-gitlfs-meta.md +++ b/docs/README-gitlfs-meta.md @@ -86,7 +86,7 @@ git lfs track "*.bin" git add foo.bin # Add metadata via a companion command -git lfs-meta track foo.bin --patient Patient/001 --specimen Specimen/XYZ +git lfs-meta tag foo.bin --patient Patient/001 --specimen Specimen/XYZ git commit -m "Add patient-associated data" git push @@ -163,7 +163,7 @@ git config alias.lfs-meta '!lfs-meta' Now you can run: ```bash -git lfs-meta track path/to/file --patient Patient/1234 +git lfs-meta tag path/to/file --patient Patient/1234 ``` --- diff --git a/docs/README-trackremote.md b/docs/README-trackremote.md new file mode 100644 index 0000000..d08576f --- /dev/null +++ b/docs/README-trackremote.md @@ -0,0 +1,97 @@ +If the user **doesn't have the SHA256 hash** of the remote file (which Git LFS requires for the pointer), but they do have an **MD5 hash** or **ETag** (common in object stores like S3), then you can implement a **two-stage mapping approach** in your Git LFS custom transfer agent. + +## πŸ” User-Friendly Bonus + +For object stores like AWS S3: +- `HEAD` requests return `ContentLength` and `ETag` β€” no download needed. +- You can cache remote metadata efficiently. +- User should just have to specify the url and the system can retrieve + + +--- + +## 🧠 Strategy: Use ETag or MD5 to Resolve to SHA256 + +Instead of requiring the user to download the file, the system can: + +### πŸ”Ή 1. **Store metadata keyed by ETag or MD5** +```json +{ + "etag": "abc123etag", + "url": "https://mybucket.s3.amazonaws.com/file.bam", + "size": 1048576, + "sha256": null +} +``` + +### πŸ”Ή 2. **During transfer (download/upload):** +- Use ETag to identify the file. +- At the **first transfer**, download the file, compute SHA256 once, and cache it. +- Store the mapping: `etag β†’ sha256` +- Update the `.lfs-meta/.json` so it can be reused. + +--- + +## βœ… Workflow + +### βš™οΈ `git lfs track-remote` (No SHA256) + +```bash +# user has attributes and specifies a local path +git lfs track-remote data/file.bam \ + --url https://mybucket.s3.amazonaws.com/file.bam \ + --etag abc123etag \ + --size 1048576 + +# user simply specifies a remote path +git lfs track-remote --url https://mybucket.s3.amazonaws.com/file.bam +# system HEADs url and retrieves: +# path = file.bam +# etag abc123etag +# size 1048576 +# TODO: specify where AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION are set + +# user specifies path and remote path +git lfs track-remote my-directory/my-name.bam --url https://mybucket.s3.amazonaws.com/file.bam + +``` + +1. Writes: + - `data/file.bam` β†’ Git LFS pointer file with **temporary SHA** (placeholder) + - `.lfs-meta/etag/abc123etag.json` β†’ URL + metadata + +2. On `git lfs pull`: + - Transfer agent: + - Resolves `etag β†’ url` + - Downloads file + - Calculates `sha256` + - Rewrites `.git/lfs/objects/...` with correct SHA + - Creates `.lfs-meta/.json` for future use + +3. Subsequent pulls/commits: + - If the file is intended to be stored in one of "our" buckets:The SHA256 is known and directly used. + - Otherwise, the transfer agent can still use the ETag to identify the file. + +--- + +## πŸ“ Directory Layout + +``` +repo/ +β”œβ”€β”€ .lfs-meta/ +β”‚ β”œβ”€β”€ etag/ +β”‚ β”‚ └── abc123etag.json # early metadata keyed by ETag +β”‚ └── sha256/ +β”‚ └── 6a7e3...json # full metadata keyed by SHA once known +└── file.bam # Git LFS pointer (eventually points to 6a7e3...) +``` + +--- + +## πŸ§‘β€πŸ’» Tips for Implementation + +- Use ETag or MD5 **as a temporary key** until the SHA256 is known. +- Populate `.lfs-meta` with: + - `etag β†’ url` + - `etag β†’ sha256` (once resolved) +- Optional: warn user if size mismatches during transfer From 59d29679eaa2187ec9b41527becf46fda30d495d Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Wed, 23 Apr 2025 17:55:38 -0700 Subject: [PATCH 06/19] cleanup --- docs/README-comparison.md | 14 +++---- docs/README-git-sync.md | 82 +++++--------------------------------- docs/README-gitlfs-meta.md | 16 +------- docs/README.md | 6 ++- 4 files changed, 22 insertions(+), 96 deletions(-) diff --git a/docs/README-comparison.md b/docs/README-comparison.md index 2686a80..a2a8df5 100644 --- a/docs/README-comparison.md +++ b/docs/README-comparison.md @@ -1,5 +1,5 @@ -# Comparison: Git LFS and g3t Integrated Data Platform (ACED-IDP) -A comparative overview of two distinct approaches to managing and storing large project data files: Git Large File Storage (Git LFS) and the ACED Integrated Data Platform (ACED-IDP). +# Comparison: Git LFS and g3t Integrated Data Platform (CALIPER-IDP) +A comparative overview of two distinct approaches to managing and storing large project data files: Git Large File Storage (Git LFS) and the CALIPER Integrated Data Platform (CALIPER-IDP). --- @@ -23,9 +23,9 @@ A comparative overview of two distinct approaches to managing and storing large --- -## ACED Integrated Data Platform (ACED-IDP) +## CALIPER Integrated Data Platform (CALIPER-IDP) -**Purpose:** ACED-IDP is a specialized data commons platform developed by the International Alliance for Cancer Early Detection (ACED) to facilitate secure and structured sharing of research data among member institutions. +**Purpose:** CALIPER-IDP is a specialized data commons platform developed by the International Alliance for Cancer Early Detection (CALIPER) to facilitate secure and structured sharing of research data among member institutions. **Key Features:** @@ -46,7 +46,7 @@ A comparative overview of two distinct approaches to managing and storing large ## Comparative Summary -| Feature | Git LFS | ACED-IDP | +| Feature | Git LFS | CALIPER-IDP | |---------------------------|--------------------------------------------------------|-----------------------------------------------------------| | **Primary Use Case** | Managing large files in software development projects | Collaborative biomedical research data management | | **Integration** | Seamless with Git workflows | Built on Gen3 framework with specialized CLI tools | @@ -61,6 +61,6 @@ A comparative overview of two distinct approaches to managing and storing large - **Git LFS** is ideal for developers seeking to manage large files within their existing Git workflows, offering a straightforward solution without the need for additional infrastructure. -- **ACED-IDP** caters to the complex needs of collaborative biomedical research, providing a robust platform for secure data sharing, standardized metadata integration, and advanced data exploration capabilities. +- **CALIPER-IDP** caters to the complex needs of collaborative biomedical research, providing a robust platform for secure data sharing, standardized metadata integration, and advanced data exploration capabilities. -The choice between Git LFS and ACED-IDP depends on the specific requirements of the project, including the nature of the data, collaboration needs, and compliance considerations. +The choice between Git LFS and CALIPER-IDP depends on the specific requirements of the project, including the nature of the data, collaboration needs, and compliance considerations. diff --git a/docs/README-git-sync.md b/docs/README-git-sync.md index fc23690..0aa9527 100644 --- a/docs/README-git-sync.md +++ b/docs/README-git-sync.md @@ -26,9 +26,9 @@ Contents: | REST API v +---------------------+ +---------------------+ - | git-sync CLI +------>+ Gen3 Access API | - | - Fetch & map roles | | - Projects & ACLs | - | - Transform to Gen3 | | - Policies | + | git-sync CLI +------> + Gen3 Access API | + | - Fetch & map roles | | - Projects & ACLs | + | - Transform to Gen3 | | - Policies | +---------------------+ +---------------------+ ``` @@ -166,68 +166,6 @@ To enable support for **GitLab** or **other Git servers**, we introduce an **abs --- -## 🧩 Interface Definition: `RoleSourceAdapter` - -```python -from typing import List, Dict - -class RoleSourceAdapter: - def get_users_for_project(self, project_id: str) -> List[str]: - raise NotImplementedError - - def get_teams_for_project(self, project_id: str) -> Dict[str, List[str]]: - raise NotImplementedError - - def get_user_roles(self) -> Dict[str, str]: - """Optional: direct user-to-role mapping""" - raise NotImplementedError -``` - ---- - -## πŸ”Œ GitHub Adapter Example - -```python -class GitHubAdapter(RoleSourceAdapter): - def __init__(self, github_token: str, org: str): - self.token = github_token - self.org = org - - def get_teams_for_project(self, project_id): - # use GitHub REST API to get team memberships - return { - "aced-project-xyz-admins": ["alice", "bob"], - "aced-project-xyz-members": ["carol", "dave"] - } - - def get_user_roles(self): - # flatten and map to Gen3 roles - return { - "alice": "project-admin", - "carol": "project-member" - } -``` - ---- - -## πŸ”Œ GitLab Adapter Example (Stub) - -```python -class GitLabAdapter(RoleSourceAdapter): - def __init__(self, token: str, group_id: str): - self.token = token - self.group_id = group_id - - def get_teams_for_project(self, project_id): - # use GitLab group and project member APIs - pass - - def get_user_roles(self): - # similar mapping logic - pass -``` - ---- ## πŸ›  CLI Usage Example @@ -335,14 +273,14 @@ Set up mocks or test containers for GitHub API and Gen3 Fence. ``` tests/ β”œβ”€β”€ unit/ -β”‚ β”œβ”€β”€ test_github_adapter.py -β”‚ β”œβ”€β”€ test_gitlab_adapter.py -β”‚ β”œβ”€β”€ test_core_logic.py -β”‚ └── test_config_loader.py +β”‚ β”œβ”€β”€ test_github_adapter +β”‚ β”œβ”€β”€ test_gitlab_adapter +β”‚ β”œβ”€β”€ test_core_logic +β”‚ └── test_config_loader β”œβ”€β”€ integration/ -β”‚ β”œβ”€β”€ test_end_to_end_github_sync.py -β”‚ β”œβ”€β”€ test_error_handling.py -β”‚ └── test_gen3_interactions.py +β”‚ β”œβ”€β”€ test_end_to_end_github_sync +β”‚ β”œβ”€β”€ test_error_handling +β”‚ └── test_gen3_interactions └── fixtures/ └── github_team_response.json ``` diff --git a/docs/README-gitlfs-meta.md b/docs/README-gitlfs-meta.md index 545fc3f..4345b42 100644 --- a/docs/README-gitlfs-meta.md +++ b/docs/README-gitlfs-meta.md @@ -117,7 +117,7 @@ git push ### πŸ“¦ 1. Install the `lfs-meta` Tool -Install globally or per-project. Example (Python-based): +Install globally or per-project. Example (GO Python-based): ```bash pip install git-lfs-meta @@ -339,17 +339,3 @@ Absolutely β€” here’s a **test specification section** for the `lfs-meta` feat | `test_no_metadata_file()` | No `.lfs-meta/metadata.json` | Graceful failure or warning message | ---- - -### πŸ“ Suggested Test Structure - -``` -tests/ -β”œβ”€β”€ unit/ -β”‚ └── test_meta_generation.py -β”œβ”€β”€ integration/ -β”‚ └── test_cli_init_meta.py -└── fixtures/ - └── sample_metadata.json -``` - diff --git a/docs/README.md b/docs/README.md index e23f566..60c7f5d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -79,13 +79,15 @@ To ensure seamless operation between these utilities: - Conduct training sessions to familiarize users with the new tools. -## πŸ“š Documentation Files +## πŸ“š Documentation Files + +The following files provide **very rough, draft** information about the `git-gen3` project, its architecture, and its components: 1. [README-comparison](README-comparison.md) - A comparison of the `lfs-meta` tool with other tools and approaches. - Discusses the advantages and disadvantages of each approach. - Provides a summary of the key features and capabilities of `lfs-meta`. -2. [Architecture and Sprint Plan](README-epic.md) +2. [Epics and Sprint Plan](README-epic.md) - Overview of project goals, sprint breakdowns, and deliverables. 3. [Auth-sync](README-git-sync.md) - Details on how to sync authentication and authorization with github/synpase/etc as the system of record for project membership. From 0fc923169d76338cb1e685807f8a38a403ff6345 Mon Sep 17 00:00:00 2001 From: Kyle Ellrott Date: Sat, 26 Apr 2025 14:19:20 -0700 Subject: [PATCH 07/19] Starting to add user story based on DRS --- docs/README-usage-story.md | 178 +++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/README-usage-story.md diff --git a/docs/README-usage-story.md b/docs/README-usage-story.md new file mode 100644 index 0000000..3f82dc4 --- /dev/null +++ b/docs/README-usage-story.md @@ -0,0 +1,178 @@ + +# Usage story + +This story tracks a propose usage pattern for an analyist interactive with +a Calypr project using the git plugin. In this case, the plugin is named git-drs, although +that may be modified in the future. + +In this case, there is an existing project defined at github.com/ohsu-comp-bio/test-project.git + +## Install plugin configuratin plugin + +```bash +$ git drs install +``` + +## clone a project + +```bash +$ git clone git@github.com:ohsu-comp-bio/test-project.git +``` + +At this point no file have been downloaded. A hidden folder tracks all document references. + +## List document references + +This lists all document references added to a project. A document reference includes: + - The name of the file + - The project relative path of the file + - The size of the file + - File identifiers, such as etags, MD5 or SHA256 + - An array of locations. This could include multiple URLs, file paths or other download methods + +```bash +$ git drs list +R ./my-data/sample1.bam +R ./my-data/sample1.bam.bai +L ./my-data/sample2.bam +M ./my-data/sample2.bam.bai +U ./my-data/sample1.vcf +G ./my-data/sample1.txt +``` + +Codes are: ++-----+-------+ +|R | Remote | +|L | Local | +|M | Modified | +|U | Untracked | +|G | Git tracked file| ++--+---------+ + + +Download a file + +```bash +$ $ git drs ls ./my-data/simple1.bam +R ./my-data/simple1.bam +$ $ git drs pull ./my-data/sample1.bam +L ./my-data/simple1.bam +``` + +Add a local file +```bash +$ git drs add ./my-data/simple1.vcf +M ./my-data/simple1.vcf +``` +In this version, the file is moved to a `Modified` state. The file will be uploaded to the default bucket for the project on `push`, at which point it will be changed to a `Local` state. + +Add a local file to a non default bucket +```bash +$ git drs add ./my-data/simple1.vcf -r alt-bucket +M ./my-data/simple1.vcf +``` + + +Add a local file that is a symbolic link to a shared folder +```bash +$ git drs add ./my-data/simple1.vcf -l /mnt/shared/results/simple1.vcf +L ./my-data/simple1.vcf +``` +In this version, the file is added as a reference, but not pushed to a project repository. A +symlnk to the actual file is added to the project folder and the state is changed to Local. + +Add an existing S3 resource to the project +```bash +$ git drs add ./my-data/simple1.vcf --s3 forterra/my-bucket/results/simple1.vcf +L ./my-data/simple1.vcf +``` +This moves the file from the `Untracked` state to `Remote` state. + +Push +move any files in the modified state to remote repositories +```bash +$ git drs ls ./data/ +R ./my-data/sample1.bam +R ./my-data/sample1.bam.bai +M ./my-data/simple1.vcf +$ git drs push +L ./my-data/simple1.vcf +``` + +# Remote management + +List repositories that are associated with project +```bash +$ git drs remote list +default gen3 calypr.ohsu.edu compbio/my-project +alternate s3 rgw.ohsu.edu MyLab +arc-local local arc.ohsu.edu,*.arc.ohsu.edu /mnt/shared/dir/ +anvil drs anvilproject.org +``` + +The output pattern is resource name, interface type, hostname, remote base path, with +default being the name of the default storage resource. +In the case of S3 objects, hostname is the server URL. For local storage entries, +the list of host names (comma delimited with * wildcards) is host names where the local file +storage should be valid + + +Add remote server +```bash +$ git drs remote add gen3 compbio/my-project +``` + + +# DRS info + +```bash +$ git drs info ./my-data/simple1.vcf +``` + +Should return something like: +```json +{ + "id": "drs://example.org/12345", + "name": "simple1.vcf", + "self_uri": "drs://example.org/12345", + "size": 2684354560, + "created_time": "2023-01-15T12:34:56Z", + "updated_time": "2023-06-20T14:22:10Z", + "version": "1.0", + "mime_type": "application/octet-stream", + "checksums": [ + { + "type": "md5", + "checksum": "1a79a4d60de6718e8e5b326e338ae533" + } + ], + "access_methods": [ + { + "type": "https", + "access_url": { + "url": "https://example.org/data/HG00096.bam" + }, + "region": "us-east-1" + } + ], + "description": "BAM file for sample HG00096 from the 1000 Genomes Project", + "aliases": [ + "1000G_HG00096_bam" + ], + "contents": [] +} +``` + + +## Storage +All DRS records will be in a folder under git top level directory in a folder named `.drs` + +```bash +$ find .drs/ +./drs/my-data/sample1.bam +./drs/my-data/sample1.bam.bai +./drs/my-data/sample2.bam +./drs/my-data/sample2.bam.bai +./drs/my-data/sample1.vcf +./drs/my-data/sample1.txt +``` \ No newline at end of file From dfadf1219fa9781ca13ba63444ff8d4f495a15a6 Mon Sep 17 00:00:00 2001 From: Kyle Ellrott Date: Sat, 26 Apr 2025 14:20:31 -0700 Subject: [PATCH 08/19] Fixing small text issue --- docs/README-usage-story.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README-usage-story.md b/docs/README-usage-story.md index 3f82dc4..5259a12 100644 --- a/docs/README-usage-story.md +++ b/docs/README-usage-story.md @@ -53,9 +53,9 @@ Codes are: Download a file ```bash -$ $ git drs ls ./my-data/simple1.bam +$ git drs ls ./my-data/simple1.bam R ./my-data/simple1.bam -$ $ git drs pull ./my-data/sample1.bam +$ git drs pull ./my-data/sample1.bam L ./my-data/simple1.bam ``` From 04d8defd93bc4f7993c39c57bb185bd347a08e34 Mon Sep 17 00:00:00 2001 From: Kyle Ellrott Date: Sun, 27 Apr 2025 11:30:02 -0700 Subject: [PATCH 09/19] Adding more details about interfacing with a DRS server --- docs/README-usage-story.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/README-usage-story.md b/docs/README-usage-story.md index 5259a12..75cb068 100644 --- a/docs/README-usage-story.md +++ b/docs/README-usage-story.md @@ -49,8 +49,7 @@ Codes are: |G | Git tracked file| +--+---------+ - -Download a file +## Download a file ```bash $ git drs ls ./my-data/simple1.bam @@ -59,7 +58,7 @@ $ git drs pull ./my-data/sample1.bam L ./my-data/simple1.bam ``` -Add a local file +## Add a local file ```bash $ git drs add ./my-data/simple1.vcf M ./my-data/simple1.vcf @@ -72,6 +71,7 @@ $ git drs add ./my-data/simple1.vcf -r alt-bucket M ./my-data/simple1.vcf ``` +## Add symlink Add a local file that is a symbolic link to a shared folder ```bash @@ -88,7 +88,13 @@ L ./my-data/simple1.vcf ``` This moves the file from the `Untracked` state to `Remote` state. -Push +## Add an Existing DRS object +```bash +$ git drs add ./my-data/simple1.vcf --drs drs://example.org/12345 +R ./my-data/simple1.vcf +``` + +# Push move any files in the modified state to remote repositories ```bash $ git drs ls ./data/ @@ -117,14 +123,28 @@ the list of host names (comma delimited with * wildcards) is host names where th storage should be valid -Add remote server +## Add remote server +```bash +$ git drs remote add gen3 origin compbio/my-project +``` +The base command `git drs remote add` takes the arguments +`type` `name` and `URL` + +## Add a DRS server ```bash -$ git drs remote add gen3 compbio/my-project +$ git drs remote add drs anvil https://drs.anvilproject.org ``` +## Add a local shared folder +This is a common drive, often network attached, that holds large common files. +When files are added or pulled from this resource, symlinks are used to point to the +original file. +```bash +$ git drs remote add shared arc-data /mnt/shared/data +``` # DRS info - +View the DRS record for a file ```bash $ git drs info ./my-data/simple1.vcf ``` From 2986b34aef22f9bb4d02698867f6df1d7d2c9680 Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 28 Apr 2025 07:50:14 -0700 Subject: [PATCH 10/19] format table --- docs/README-usage-story.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/README-usage-story.md b/docs/README-usage-story.md index 75cb068..44d72a9 100644 --- a/docs/README-usage-story.md +++ b/docs/README-usage-story.md @@ -41,13 +41,15 @@ G ./my-data/sample1.txt ``` Codes are: -+-----+-------+ + +| code| meaning | +|-----|-------| |R | Remote | |L | Local | |M | Modified | |U | Untracked | |G | Git tracked file| -+--+---------+ + ## Download a file @@ -195,4 +197,4 @@ $ find .drs/ ./drs/my-data/sample2.bam.bai ./drs/my-data/sample1.vcf ./drs/my-data/sample1.txt -``` \ No newline at end of file +``` From db3ad450993b45260f8abc77c4b0e5a63634c98f Mon Sep 17 00:00:00 2001 From: Brian Date: Mon, 28 Apr 2025 10:10:26 -0700 Subject: [PATCH 11/19] Update README.md --- docs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/README.md b/docs/README.md index 60c7f5d..c9b08c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ Welcome to the `git-gen3` documentation! Below is an index of the available docu --- ## Overview -Based on the current structure of the ACED Integrated Data Platform (IDP), which utilizes the `g3t` command-line tool for project creation, file uploads, and metadata association ξˆ€citeξˆ‚turn0search0, it's advisable to refactor this monolithic approach into modular utilities. This will enhance maintainability, scalability, and facilitate targeted enhancements. +Based on the current structure of the ACED Integrated Data Platform (IDP), which utilizes the `g3t` command-line tool for project creation, file uploads, and metadata association, it's advisable to refactor this monolithic approach into modular utilities. This will enhance maintainability, scalability, and facilitate targeted enhancements. --- @@ -101,4 +101,4 @@ The following files provide **very rough, draft** information about the `git-gen - Guidelines for testing the release process and ensuring functionality. ## Overview -![](images/gen3-lfs.png) \ No newline at end of file +![](images/gen3-lfs.png) From 3f073067a3b4437b65a4afb4d57b9328c7012ab0 Mon Sep 17 00:00:00 2001 From: Brian Walsh Date: Mon, 28 Apr 2025 14:33:15 -0700 Subject: [PATCH 12/19] adds hybrid-oid sha256 --- docs/README-hybrid-oid.md | 154 ++++++++++++++++++++++++++++++++++++++ docs/README.md | 3 +- 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 docs/README-hybrid-oid.md diff --git a/docs/README-hybrid-oid.md b/docs/README-hybrid-oid.md new file mode 100644 index 0000000..aea7a3e --- /dev/null +++ b/docs/README-hybrid-oid.md @@ -0,0 +1,154 @@ +If the user **doesn't have the SHA256 hash** of the remote file (which Git LFS requires for the pointer), but they do have an **MD5 hash** or **ETag** (common in object stores like S3), then you can implement a **two-stage mapping approach** in your Git LFS custom transfer agent. + +--- + +## 🧠 Strategy: Use ETag or MD5 to Resolve to SHA256 + +> TODO - 🚧 this needs prototyping - completely untested 🚧 + +Instead of requiring the user to download the file, your system can: + +### πŸ”Ή 1. **Store metadata keyed by ETag or MD5** +```json +{ + "etag": "abc123etag", + "url": "https://mybucket.s3.amazonaws.com/file.bam", + "size": 1048576, + "sha256": null +} +``` + +### πŸ”Ή 2. **During transfer (download/upload):** +- Use ETag to identify the file. +- At the **first transfer**, download the file, compute SHA256 once, and cache it. +- Store the mapping: `etag β†’ sha256` +- Update the `.lfs-meta/.json` so it can be reused. + +--- + +## βœ… Workflow + +### βš™οΈ `git lfs track-remote` (No SHA256) + +```bash +git lfs track-remote data/file.bam \ + --url https://mybucket.s3.amazonaws.com/file.bam \ + --etag abc123etag \ + --size 1048576 +``` + +1. Writes: + - `data/file.bam` β†’ Git LFS pointer file with **temporary SHA** (placeholder) + - `.lfs-meta/etag/abc123etag.json` β†’ URL + metadata + +2. On `git lfs pull`: + - Transfer agent: + - Resolves `etag β†’ url` + - Downloads file + - Calculates `sha256` + - Rewrites `.git/lfs/objects/...` with correct SHA + - Creates `.lfs-meta/.json` for future use + +3. Subsequent pulls/commits: + - The SHA256 is known and directly used. + +--- + +## πŸ“ Directory Layout + +``` +repo/ +β”œβ”€β”€ .lfs-meta/ +β”‚ β”œβ”€β”€ etag/ +β”‚ β”‚ └── abc123etag.json # early metadata keyed by ETag +β”‚ └── sha256/ +β”‚ └── 6a7e3...json # full metadata keyed by SHA once known +└── file.bam # Git LFS pointer (eventually points to 6a7e3...) +``` + +--- + +## πŸ§‘β€πŸ’» Tips for Implementation + +- Use ETag or MD5 **as a temporary key** until the SHA256 is known. +- Populate `.lfs-meta` with: + - `etag β†’ url` + - `etag β†’ sha256` (once resolved) +- Optional: warn user if size mismatches during transfer +- You can support `track-remote` with: + ```bash + --etag abc123etag + --size 1048576 + ``` + +--- + +## πŸ” Cloud-Friendly Bonus + +For object stores like AWS S3: +- `HEAD` requests return `ContentLength` and `ETag` β€” no download needed. +- You can cache remote metadata efficiently. + +--- +If the user wants to **mix standard Git LFS files** (stored in a Git LFS server or local LFS cache) with **custom β€œremote” LFS files** (tracked via metadata like ETag/URL), the best approach is to **register multiple transfer agents and selectively route files** to the right agent based on their OID or file path. + +--- + +## 🧭 Strategy Overview + +1. **Standard LFS files** are handled by the default `basic` agent. +2. **Remote-tracked files** are handled by your custom agent (e.g., `remote`), using metadata like ETag or MD5. +3. Use **OID prefixes** (e.g., `etag-abc123`) or filename patterns to differentiate. + +--- + +## βœ… Use Custom OID Prefix (Recommended) + +### πŸ”‘ Idea: +When registering a remote file via `track-remote`, prefix its OID with `etag-` instead of a real SHA256. Your custom agent handles these, while standard files still use SHA-based OIDs. + +### `.gitconfig` +```ini +[lfs.customtransfer] + remote.path = python3 transfer_agent.py + +[lfs] + concurrenttransfers = 3 + tusTransferMaxRetries = 1 + transfer = remote,basic # order matters +``` + +### In `transfer_agent.py`, match only `etag-*` OIDs: + +```python +if cmd["event"] == "download" and cmd["operation"]["oid"].startswith("etag-"): + ... +``` + +### Standard files (with SHA256 OIDs) bypass this agent and fall back to `basic`. + +--- + +## πŸ” Hybrid Considerations + +| Concern | Standard LFS | Remote LFS (custom) | +|------------------------|--------------|----------------------| +| SHA256 available | Yes | Optional (resolved on pull) | +| Pointer format | Standard | Compatible, but custom `oid` | +| Transfer storage | Git LFS server | External (e.g., S3, HTTP) | +| Pull/Push supported | Yes | Yes (via agent) | +| Integrity verification | SHA256 | SHA256 (on first download) | + +--- + +## πŸš€ Summary + +| Use Case | Solution | +|----------------------------|-----------------------------------------------| +| Mixed LFS file support | Register multiple agents (`remote`, `basic`) | +| Route remote files | Use `oid` prefix like `etag-*` | +| Route standard files | Leave `oid` as normal SHA256 | +| Optional: path-based split | Use `.gitattributes` with multiple filters | + +--- + diff --git a/docs/README.md b/docs/README.md index c9b08c8..ba2b873 100644 --- a/docs/README.md +++ b/docs/README.md @@ -81,7 +81,7 @@ To ensure seamless operation between these utilities: ## πŸ“š Documentation Files -The following files provide **very rough, draft** information about the `git-gen3` project, its architecture, and its components: +The following files provide **very rough, draft 🚧 ** information about the `git-gen3` project, its architecture, and its components: 1. [README-comparison](README-comparison.md) - A comparison of the `lfs-meta` tool with other tools and approaches. @@ -97,6 +97,7 @@ The following files provide **very rough, draft** information about the `git-gen - Information on how to manage metadata for large files in Git. 6. [Git LFS Remote Buckets](README-gitlfs-remote-buckets.md) - Details for tracking remote files without downloading them. + - See also [Hybrid OID](README-hybrid-oid.md) for more information. 7. [Release Testing](README-release-test.md) - Guidelines for testing the release process and ensuring functionality. From b218d75b5a64e16cdb7eda040cf6dcfb3d252340 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 29 Apr 2025 12:19:34 -0700 Subject: [PATCH 13/19] Update README.md --- docs/README.md | 46 ++++++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/docs/README.md b/docs/README.md index ba2b873..a27a009 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,39 +15,47 @@ Transitioning to a modular architecture involves decomposing the monolith into d ### 1. **Project Management Utility** **Responsibilities:** --Initialize and manage project structures --Handle user roles and permissions --Integrate with git servers for audit trails project membership + +- Initialize and manage project structures +- Handle user roles and permissions +- Integrate with git servers for audit trails project membership **Implementation Suggestions:** --Develop a CLI tool, e.g., `auth-sync`, to manage project lifecycles --Utilize configuration files (YAML/JSON) to define project metadata --Integrate with Git for version control and collaboration + +- Develop a CLI tool, e.g., `auth-sync`, to manage project lifecycles +- Utilize configuration files (YAML/JSON) to define project metadata +- Integrate with Git for version control and collaboration ### 2. **File Transfer Utility** **Responsibilities:** --Handle uploading and downloading of data files --Support resumable transfers and integrity checks --Manage storage backend interactions (e.g., S3, GCS) + +- Handle uploading and downloading of data files +- Support resumable transfers and integrity checks +- Manage storage backend interactions (e.g., S3, GCS) **Implementation Suggestions:** --Create a tools, e.g., `git-lfs extentions, git add/pull url`, to abstract file operations --Incorporate support for various storage backends using plugins or adapters --Implement checksum verification to ensure data integrity + +- Create a tools, e.g., `git-lfs extentions, git add/pull url`, to abstract file operations +- Incorporate support for various storage backends using plugins or adapters +- Implement checksum verification to ensure data integrity ### 3. **Metadata Management Utility** **Responsibilities:** --Facilitate the creation, validation, and submission of metadata --Transform metadata into required formats (e.g., FHIR resources) --Ensure compliance with data standards and schemas + +- Facilitate the creation, validation, and submission of metadata +- Transform metadata into required formats (e.g., FHIR resources) +- Ensure compliance with data standards and schemas +- See [General Discussion](https://github.com/bmeg/git-gen3/pull/3#issuecomment-2835614773) +- See [Bulk tagging](https://github.com/bmeg/git-gen3/pull/3#issuecomment-2835728266) **Implementation Suggestions:** --Develop a utility, e.g., `git meta init/validate/etc`, to manage metadata workflows --Leverage existing tools like `g3t_etl` for data transformation --Incorporate schema validation to enforce data quality + +- Develop a utility, e.g., `git meta init/validate/etc`, to manage metadata workflows +- Leverage existing tools like `g3t_etl` for data transformation +- Incorporate schema validation to enforce data quality --- @@ -87,6 +95,8 @@ The following files provide **very rough, draft 🚧 ** information about the `g - A comparison of the `lfs-meta` tool with other tools and approaches. - Discusses the advantages and disadvantages of each approach. - Provides a summary of the key features and capabilities of `lfs-meta`. + - [DRS Useage Story](README-usage-story.md) + - [Comparison with git lfs](https://github.com/bmeg/git-gen3/pull/3#issuecomment-2835614773) 2. [Epics and Sprint Plan](README-epic.md) - Overview of project goals, sprint breakdowns, and deliverables. 3. [Auth-sync](README-git-sync.md) From e9fac72c93ee44922346013c6567b8c5c3a46157 Mon Sep 17 00:00:00 2001 From: Brian Date: Tue, 29 Apr 2025 12:22:34 -0700 Subject: [PATCH 14/19] Update README-epic.md --- docs/README-epic.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README-epic.md b/docs/README-epic.md index e1a9914..1a0fee1 100644 --- a/docs/README-epic.md +++ b/docs/README-epic.md @@ -14,6 +14,7 @@ De-risk implementation by validating core architectural assumptions and tool com ### πŸ”¬ Tasks: | ID | Task Description | Est. | |-------|------------------------------------------------------------------------------|------| +| SPK-0 | Learning - team spends time, becomes familiar with "stock" git-lfs | 1d | | SPK-1 | Prototype `track-remote` to fetch metadata (e.g., ETag, size) from S3/GCS | 1d | | SPK-2 | Simulate `.lfs-meta/metadata.json` usage in Git repo + commit/push | 0.5d | | SPK-3 | Test `init-meta` to produce `DocumentReference.ndjson` via `g3t`-style logic | 1d | @@ -23,6 +24,7 @@ De-risk implementation by validating core architectural assumptions and tool com | SPK-7 | Validate `gen3-client` can UChicago's go code be installed and called? | 4d | ### βœ… Deliverables: + - Prototype CLI for `track-remote` - not currently part of got-lfs - How are user credentials handled? - Sample `.lfs-meta/metadata.json` and generated `META/DocumentReference.ndjson` From c6f9ff8a0b68bb4ea1d0c85dc01e8846eac2676f Mon Sep 17 00:00:00 2001 From: Kyle Ellrott Date: Wed, 7 May 2025 09:12:25 -0700 Subject: [PATCH 15/19] Experimenting with integrating patterns from git-lfs --- cmd/add/main.go | 28 ++++++++ cmd/filterprocess/main.go | 59 +++++++++++++++++ cmd/list/main.go | 4 +- cmd/root.go | 14 ++-- cmd/track/main.go | 21 ++++++ docs/README-git-plugin-dev.md | 25 +++++++ drs/README.md | 119 ++++++++++++++++++++++++++++++++++ drs/object.go | 44 +++++++++++++ drs/util.go | 53 +++++++++++++++ git-gen3.go => git-drs.go | 6 +- go.mod | 29 ++++++++- go.sum | 84 ++++++++++++++++++++++++ utils/common.go | 5 ++ utils/util.go | 34 ++++++++++ 14 files changed, 514 insertions(+), 11 deletions(-) create mode 100644 cmd/add/main.go create mode 100644 cmd/filterprocess/main.go create mode 100644 cmd/track/main.go create mode 100644 docs/README-git-plugin-dev.md create mode 100644 drs/README.md create mode 100644 drs/object.go create mode 100644 drs/util.go rename git-gen3.go => git-drs.go (59%) create mode 100644 utils/common.go create mode 100644 utils/util.go diff --git a/cmd/add/main.go b/cmd/add/main.go new file mode 100644 index 0000000..3ea7201 --- /dev/null +++ b/cmd/add/main.go @@ -0,0 +1,28 @@ +package add + +import ( + "fmt" + "path/filepath" + + "github.com/spf13/cobra" +) + +// Cmd line declaration +var Cmd = &cobra.Command{ + Use: "add", + Short: "Add a file", + Long: ``, + Args: cobra.MinimumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + for _, fileArg := range args { + matches, err := filepath.Glob(fileArg) + if err == nil { + for _, f := range matches { + + fmt.Printf("Adding %s\n", f) + } + } + } + return nil + }, +} diff --git a/cmd/filterprocess/main.go b/cmd/filterprocess/main.go new file mode 100644 index 0000000..70fda3b --- /dev/null +++ b/cmd/filterprocess/main.go @@ -0,0 +1,59 @@ +package filterprocess + +import ( + "fmt" + "io" + "log" + "os" + + "github.com/git-lfs/git-lfs/v3/git" + "github.com/spf13/cobra" +) + +// Cmd line declaration +var Cmd = &cobra.Command{ + Use: "filter-process", + Short: "filter proces", + Long: ``, + Args: cobra.MinimumNArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + s := git.NewFilterProcessScanner(os.Stdin, os.Stdout) + err := s.Init() + if err != nil { + return err + } + + caps, err := s.NegotiateCapabilities() + if err != nil { + return err + } + log.Printf("Caps: %#v\n", caps) + log.Printf("Running filter-process: %s\n", args) + + for s.Scan() { + req := s.Request() + switch req.Header["command"] { + case "clean": + log.Printf("Request to clean %#v %s\n", req.Payload, req.Header["pathname"]) + + clean(os.Stdout, req.Payload, req.Header["pathname"], -1) + + case "smudge": + log.Printf("Request to smudge %s %s\n", req.Payload, req.Header["pathname"]) + case "list_available_blobs": + log.Printf("Request for list_available_blobs\n") + + default: + return fmt.Errorf("don't know what to do: %s", req.Header["command"]) + } + log.Printf("Request: %#v\n", req) + } + + return nil + }, +} + +func clean(to io.Writer, from io.Reader, fileName string, fileSize int64) error { + + return nil +} diff --git a/cmd/list/main.go b/cmd/list/main.go index f6deccb..27f36ae 100644 --- a/cmd/list/main.go +++ b/cmd/list/main.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - "github.com/bmeg/git-gen3/git" + "github.com/bmeg/git-drs/utils" "github.com/spf13/cobra" ) @@ -17,7 +17,7 @@ var Cmd = &cobra.Command{ Long: ``, Args: cobra.MinimumNArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - gitTop, err := git.GitTopLevel() + gitTop, err := utils.GitTopLevel() if err != nil { fmt.Printf("Error: %s\n", err) return err diff --git a/cmd/root.go b/cmd/root.go index 9afb30e..7c87057 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -3,16 +3,18 @@ package cmd import ( "os" - "github.com/bmeg/git-gen3/cmd/initialize" - "github.com/bmeg/git-gen3/cmd/list" - "github.com/bmeg/git-gen3/cmd/pull" - "github.com/bmeg/git-gen3/cmd/push" + "github.com/bmeg/git-drs/cmd/add" + "github.com/bmeg/git-drs/cmd/filterprocess" + "github.com/bmeg/git-drs/cmd/initialize" + "github.com/bmeg/git-drs/cmd/list" + "github.com/bmeg/git-drs/cmd/pull" + "github.com/bmeg/git-drs/cmd/push" "github.com/spf13/cobra" ) // RootCmd represents the root command var RootCmd = &cobra.Command{ - Use: "git-gen3", + Use: "git-drs", SilenceErrors: true, SilenceUsage: true, PersistentPreRun: func(cmd *cobra.Command, args []string) { @@ -25,6 +27,8 @@ func init() { RootCmd.AddCommand(push.Cmd) RootCmd.AddCommand(pull.Cmd) RootCmd.AddCommand(list.Cmd) + RootCmd.AddCommand(add.Cmd) + RootCmd.AddCommand(filterprocess.Cmd) RootCmd.AddCommand(genBashCompletionCmd) } diff --git a/cmd/track/main.go b/cmd/track/main.go new file mode 100644 index 0000000..31d1964 --- /dev/null +++ b/cmd/track/main.go @@ -0,0 +1,21 @@ +package track + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// Cmd line declaration +var Cmd = &cobra.Command{ + Use: "track", + Short: "Set a file track filter", + Long: ``, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + for i := range args { + fmt.Printf("Track %s\n", args[i]) + } + return nil + }, +} diff --git a/docs/README-git-plugin-dev.md b/docs/README-git-plugin-dev.md new file mode 100644 index 0000000..5ab7885 --- /dev/null +++ b/docs/README-git-plugin-dev.md @@ -0,0 +1,25 @@ + +# Notes about the development of git plugins + + +To attach the plugin into the configutation. In the global config `~/.gitconfig` add the lines: +``` +[filter "drs"] + clean = git-drs clean -- %f + smudge = git-drs smudge -- %f + process = git-drs filter-process + required = true +``` + +Then to add tracking in a project, add entries to `.gitattributes` in the working directory. Example: +``` +*.tsv filter=drs diff=drs merge=drs -text +``` + +For when `git status` or `git diff` are invoked on `*.tsv` file, the process `git-drs filter-process` will be +invoked. The communication between git and the subprocess is outlined in (gitprotocol-common)[https://git-scm.com/docs/gitprotocol-common]. A library for parsing this event stream is part of the git-lfs code base https://github.com/git-lfs/git-lfs/blob/main/git/filter_process_scanner.go +An example of responding to these requests can be found at https://github.com/git-lfs/git-lfs/blob/main/commands/command_filter_process.go + +My understanding: The main set of command the the filter-process command responds to are `clean` and `smudge`. +The `clean` process cleans an input document before running diff, things like run auto formatting before committing. This is where the change from the file to the remote data pointer could take place. An example of the +clean process can be found at https://github.com/git-lfs/git-lfs/blob/main/commands/command_clean.go#L27 \ No newline at end of file diff --git a/drs/README.md b/drs/README.md new file mode 100644 index 0000000..f18d028 --- /dev/null +++ b/drs/README.md @@ -0,0 +1,119 @@ + + +DRS OpenAPI definition + +```yaml +type: object +required: + - id + - self_uri + - size + - created_time + - checksums +properties: + id: + type: string + description: An identifier unique to this `DrsObject` + name: + type: string + description: |- + A string that can be used to name a `DrsObject`. + This string is made up of uppercase and lowercase letters, decimal digits, hyphen, period, and underscore [A-Za-z0-9.-_]. See http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282[portable filenames]. + self_uri: + type: string + description: |- + A drs:// hostname-based URI, as defined in the DRS documentation, that tells clients how to access this object. + The intent of this field is to make DRS objects self-contained, and therefore easier for clients to store and pass around. For example, if you arrive at this DRS JSON by resolving a compact identifier-based DRS URI, the `self_uri` presents you with a hostname and properly encoded DRS ID for use in subsequent `access` endpoint calls. + example: + drs://drs.example.org/314159 + size: + type: integer + format: int64 + description: |- + For blobs, the blob size in bytes. + For bundles, the cumulative size, in bytes, of items in the `contents` field. + created_time: + type: string + format: date-time + description: |- + Timestamp of content creation in RFC3339. + (This is the creation time of the underlying content, not of the JSON object.) + updated_time: + type: string + format: date-time + description: >- + Timestamp of content update in RFC3339, identical to `created_time` in systems + that do not support updates. + (This is the update time of the underlying content, not of the JSON object.) + version: + type: string + description: >- + A string representing a version. + + (Some systems may use checksum, a RFC3339 timestamp, or an incrementing version number.) + mime_type: + type: string + description: A string providing the mime-type of the `DrsObject`. + example: + application/json + checksums: + type: array + minItems: 1 + items: + $ref: './Checksum.yaml' + description: >- + The checksum of the `DrsObject`. At least one checksum must be provided. + + For blobs, the checksum is computed over the bytes in the blob. + + For bundles, the checksum is computed over a sorted concatenation of the + checksums of its top-level contained objects (not recursive, names not included). + The list of checksums is sorted alphabetically (hex-code) before concatenation + and a further checksum is performed on the concatenated checksum value. + + For example, if a bundle contains blobs with the following checksums: + + md5(blob1) = 72794b6d + + md5(blob2) = 5e089d29 + + Then the checksum of the bundle is: + + md5( concat( sort( md5(blob1), md5(blob2) ) ) ) + + = md5( concat( sort( 72794b6d, 5e089d29 ) ) ) + + = md5( concat( 5e089d29, 72794b6d ) ) + + = md5( 5e089d2972794b6d ) + + = f7a29a04 + access_methods: + type: array + minItems: 1 + items: + $ref: './AccessMethod.yaml' + description: |- + The list of access methods that can be used to fetch the `DrsObject`. + Required for single blobs; optional for bundles. + contents: + type: array + description: >- + If not set, this `DrsObject` is a single blob. + + If set, this `DrsObject` is a bundle containing the listed `ContentsObject` s (some of which may be further nested). + items: + $ref: './ContentsObject.yaml' + description: + type: string + description: A human readable description of the `DrsObject`. + aliases: + type: array + items: + type: string + description: >- + A list of strings that can be used to find other metadata + about this `DrsObject` from external metadata sources. These + aliases can be used to represent secondary + accession numbers or external GUIDs. +``` \ No newline at end of file diff --git a/drs/object.go b/drs/object.go new file mode 100644 index 0000000..56072cc --- /dev/null +++ b/drs/object.go @@ -0,0 +1,44 @@ +package drs + +type Checksum struct { + Checksum string `json:"checksum"` + Type string `json:"type"` +} + +type AccessURL struct { + URL string `json:"url"` + Headers []string `json:"headers"` +} + +type Authorizations struct { + //This structue is not stored in the file system +} + +type AccessMethod struct { + Type string `json:"type"` + AccessURL AccessURL `json:"access_url"` + AccessID string `json:"access_id"` + Cloud string `json:"cloud"` + Region string `json:"region"` + Avalible string `json:"available"` + Authorizations Authorizations `json:"Authorizations"` +} + +type Contents struct { +} + +type DRSObject struct { + Id string `json:"id"` + Name string `json:"name"` + SelfURL string `json:"self_url"` + Size int64 `json:"size"` + CreatedTime string `json:"created_time"` + UpdatedTime string `json:"updated_time"` + Version string `json:"version"` + MimeType string `json:"mime_type"` + Checksums []Checksum `json:"checksum"` + AccessMethods []AccessMethod `json:"access_methods"` + Contents []Contents `json:"contents"` + Description string `json:"description"` + Aliases []string `json:"aliases"` +} diff --git a/drs/util.go b/drs/util.go new file mode 100644 index 0000000..8bde784 --- /dev/null +++ b/drs/util.go @@ -0,0 +1,53 @@ +package drs + +import ( + "encoding/json" + "io/fs" + "os" + "path/filepath" + + "github.com/bmeg/git-drs/utils" +) + +const DRS_DIR = ".drs" + +type DrsWalkFunc func(path string, d *DRSObject) error + +func BaseDir() (string, error) { + gitTopLevel, err := utils.GitTopLevel() + if err != nil { + return "", err + } + return filepath.Join(gitTopLevel, DRS_DIR), nil +} + +type dirWalker struct { + baseDir string + userFunc DrsWalkFunc +} + +func (d *dirWalker) call(path string, dir fs.DirEntry, cErr error) error { + data, err := os.ReadFile(path) + if err != nil { + return nil + } + obj := DRSObject{} + err = json.Unmarshal(data, &obj) + if err != nil { + return err + } + relPath, err := filepath.Rel(d.baseDir, path) + if err != nil { + return err + } + return d.userFunc(relPath, &obj) +} + +func ObjectWalk(f DrsWalkFunc) error { + baseDir, err := BaseDir() + if err != nil { + return err + } + ud := dirWalker{baseDir, f} + return filepath.WalkDir(baseDir, ud.call) +} diff --git a/git-gen3.go b/git-drs.go similarity index 59% rename from git-gen3.go rename to git-drs.go index a6b9838..237cfd1 100644 --- a/git-gen3.go +++ b/git-drs.go @@ -1,15 +1,15 @@ package main import ( - "fmt" + "log" "os" - "github.com/bmeg/git-gen3/cmd" + "github.com/bmeg/git-drs/cmd" ) func main() { if err := cmd.RootCmd.Execute(); err != nil { - fmt.Println("Error:", err.Error()) + log.Println("Error:", err.Error()) os.Exit(1) } } diff --git a/go.mod b/go.mod index e9c6287..4e11a3f 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,36 @@ -module github.com/bmeg/git-gen3 +module github.com/bmeg/git-drs go 1.24.0 require ( + github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect + github.com/avast/retry-go v2.4.2+incompatible // indirect + github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430 // indirect + github.com/git-lfs/git-lfs/v3 v3.6.1 // indirect + github.com/git-lfs/gitobj/v2 v2.1.1 // indirect + github.com/git-lfs/go-netrc v0.0.0-20210914205454-f0c862dd687a // indirect + github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 // indirect + github.com/git-lfs/wildmatch/v2 v2.0.1 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.0.0 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect + github.com/jmhodges/clock v1.2.0 // indirect + github.com/leonelquinteros/gotext v1.5.0 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect + github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 // indirect + github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/ssgelm/cookiejarparser v1.0.1 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index ffae55e..f79b8f3 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,94 @@ +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/avast/retry-go v2.4.2+incompatible h1:+ZjCypQT/CyP0kyJO2EcU4d/ZEJWSbP8NENI578cPmA= +github.com/avast/retry-go v2.4.2+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430 h1:oempk9HjNt6rVKyKmpdnoN7XABQv3SXLWu3pxUI7Vlk= +github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430/go.mod h1:AVSs/gZKt1bOd2AhkhbS7Qh56Hv7klde22yXVbwYJhc= +github.com/git-lfs/git-lfs/v3 v3.6.1 h1:0RA2HzkMVl69KE5zCGY1PxqkDSbd/f/O7Du6CNkTYtY= +github.com/git-lfs/git-lfs/v3 v3.6.1/go.mod h1:1YO3nafGw2wKBR5LTZ7/LXJ7U7ELdvIGvcCBrLt6mfM= +github.com/git-lfs/gitobj/v2 v2.1.1 h1:tf/VU6zL1kxa3he+nf6FO/syX+LGkm6WGDsMpfuXV7Q= +github.com/git-lfs/gitobj/v2 v2.1.1/go.mod h1:q6aqxl6Uu3gWsip5GEKpw+7459F97er8COmU45ncAxw= +github.com/git-lfs/go-netrc v0.0.0-20210914205454-f0c862dd687a h1:6pskVZacdMUL93pCpMAYnMDLjH1yDFhssPYGe32sjdk= +github.com/git-lfs/go-netrc v0.0.0-20210914205454-f0c862dd687a/go.mod h1:70O4NAtvWn1jW8V8V+OKrJJYcxDLTmIozfi2fmSz5SI= +github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 h1:riQhgheTL7tMF4d5raz9t3+IzoR1i1wqxE1kZC6dY+U= +github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= +github.com/git-lfs/wildmatch/v2 v2.0.1 h1:Ds+aobrV5bK0wStILUOn9irllPyf9qrFETbKzwzoER8= +github.com/git-lfs/wildmatch/v2 v2.0.1/go.mod h1:EVqonpk9mXbREP3N8UkwoWdrF249uHpCUo5CPXY81gw= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= +github.com/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914FOauSkBM= +github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= +github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 h1:chPfVn+gpAM5CTpTyVU9j8J+xgRGwmoDlNDLjKnJiYo= +github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0= +github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/ssgelm/cookiejarparser v1.0.1 h1:cRdXauUbOTFzTPJFaeiWbHnQ+tRGlpKKzvIK9PUekE4= +github.com/ssgelm/cookiejarparser v1.0.1/go.mod h1:DUfC0mpjIzlDN7DzKjXpHj0qMI5m9VrZuz3wSlI+OEI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utils/common.go b/utils/common.go new file mode 100644 index 0000000..cd016f2 --- /dev/null +++ b/utils/common.go @@ -0,0 +1,5 @@ +package utils + +const ( + DRS_DIR = ".drs" +) diff --git a/utils/util.go b/utils/util.go new file mode 100644 index 0000000..3f6d71a --- /dev/null +++ b/utils/util.go @@ -0,0 +1,34 @@ +package utils + +import ( + "bytes" + "os/exec" + "path/filepath" + "strings" +) + +func GitTopLevel() (string, error) { + path, err := SimpleRun([]string{"git", "rev-parse", "--show-toplevel"}) + path = strings.TrimSuffix(path, "\n") + return path, err +} + +func SimpleRun(cmds []string) (string, error) { + exePath, err := exec.LookPath(cmds[0]) + if err != nil { + return "", err + } + buf := &bytes.Buffer{} + cmd := exec.Command(exePath, cmds[1:]...) + cmd.Stdout = buf + err = cmd.Run() + return buf.String(), err +} + +func DrsTopLevel() (string, error) { + base, err := GitTopLevel() + if err != nil { + return "", err + } + return filepath.Join(base, DRS_DIR), nil +} From be0294c1aac7aa74dade90c8166bbf1c5e1066f6 Mon Sep 17 00:00:00 2001 From: Kyle Ellrott Date: Thu, 8 May 2025 16:41:32 -0700 Subject: [PATCH 16/19] Adding DRS query test to code --- cmd/query/main.go | 37 ++++++++++++++++++++++++++++++ cmd/root.go | 2 ++ docs/README-git-plugin-dev.md | 4 ++-- drs/client.go | 43 +++++++++++++++++++++++++++++++++++ drs/object.go | 32 +++++++++++++------------- 5 files changed, 100 insertions(+), 18 deletions(-) create mode 100644 cmd/query/main.go create mode 100644 drs/client.go diff --git a/cmd/query/main.go b/cmd/query/main.go new file mode 100644 index 0000000..189a29d --- /dev/null +++ b/cmd/query/main.go @@ -0,0 +1,37 @@ +package query + +import ( + "encoding/json" + "fmt" + + "github.com/bmeg/git-drs/drs" + "github.com/spf13/cobra" +) + +var server string = "https://calypr.ohsu.edu/ga4gh" + +// Cmd line declaration +var Cmd = &cobra.Command{ + Use: "query", + Short: "Query server for DRS ID", + Long: ``, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + client, err := drs.NewClient(server) + if err != nil { + return err + } + + obj, err := client.GetObject(args[0]) + if err != nil { + return err + } + out, err := json.MarshalIndent(*obj, "", " ") + if err != nil { + return err + } + fmt.Printf("%s\n", string(out)) + return nil + }, +} diff --git a/cmd/root.go b/cmd/root.go index 7c87057..e0b2b56 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/bmeg/git-drs/cmd/list" "github.com/bmeg/git-drs/cmd/pull" "github.com/bmeg/git-drs/cmd/push" + "github.com/bmeg/git-drs/cmd/query" "github.com/spf13/cobra" ) @@ -29,6 +30,7 @@ func init() { RootCmd.AddCommand(list.Cmd) RootCmd.AddCommand(add.Cmd) RootCmd.AddCommand(filterprocess.Cmd) + RootCmd.AddCommand(query.Cmd) RootCmd.AddCommand(genBashCompletionCmd) } diff --git a/docs/README-git-plugin-dev.md b/docs/README-git-plugin-dev.md index 5ab7885..f33fa15 100644 --- a/docs/README-git-plugin-dev.md +++ b/docs/README-git-plugin-dev.md @@ -17,9 +17,9 @@ Then to add tracking in a project, add entries to `.gitattributes` in the workin ``` For when `git status` or `git diff` are invoked on `*.tsv` file, the process `git-drs filter-process` will be -invoked. The communication between git and the subprocess is outlined in (gitprotocol-common)[https://git-scm.com/docs/gitprotocol-common]. A library for parsing this event stream is part of the git-lfs code base https://github.com/git-lfs/git-lfs/blob/main/git/filter_process_scanner.go +invoked. The communication between git and the subprocess is outlined in [gitprotocol-common](https://git-scm.com/docs/gitprotocol-common). A library for parsing this event stream is part of the git-lfs code base https://github.com/git-lfs/git-lfs/blob/main/git/filter_process_scanner.go An example of responding to these requests can be found at https://github.com/git-lfs/git-lfs/blob/main/commands/command_filter_process.go My understanding: The main set of command the the filter-process command responds to are `clean` and `smudge`. The `clean` process cleans an input document before running diff, things like run auto formatting before committing. This is where the change from the file to the remote data pointer could take place. An example of the -clean process can be found at https://github.com/git-lfs/git-lfs/blob/main/commands/command_clean.go#L27 \ No newline at end of file +clean process can be found at https://github.com/git-lfs/git-lfs/blob/main/commands/command_clean.go#L27 diff --git a/drs/client.go b/drs/client.go new file mode 100644 index 0000000..0b7bac7 --- /dev/null +++ b/drs/client.go @@ -0,0 +1,43 @@ +package drs + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "path/filepath" +) + +type Client struct { + base *url.URL +} + +func NewClient(base string) (*Client, error) { + baseURL, err := url.Parse(base) + return &Client{baseURL}, err +} + +func (cl *Client) GetObject(id string) (*DRSObject, error) { + + a := *cl.base + a.Path = filepath.Join(a.Path, "drs/v1/objects", id) + + response, err := http.Get(a.String()) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + //log.Printf("Getting URL %s\n", a.String()) + //fmt.Printf("%s\n", string(body)) + + out := DRSObject{} + err = json.Unmarshal(body, &out) + if err != nil { + return nil, err + } + return &out, nil +} diff --git a/drs/object.go b/drs/object.go index 56072cc..2199e3c 100644 --- a/drs/object.go +++ b/drs/object.go @@ -15,13 +15,13 @@ type Authorizations struct { } type AccessMethod struct { - Type string `json:"type"` - AccessURL AccessURL `json:"access_url"` - AccessID string `json:"access_id"` - Cloud string `json:"cloud"` - Region string `json:"region"` - Avalible string `json:"available"` - Authorizations Authorizations `json:"Authorizations"` + Type string `json:"type"` + AccessURL AccessURL `json:"access_url"` + AccessID string `json:"access_id,omitempty"` + Cloud string `json:"cloud,omitempty"` + Region string `json:"region,omitempty"` + Avalible string `json:"available,omitempty"` + Authorizations *Authorizations `json:"Authorizations,omitempty"` } type Contents struct { @@ -30,15 +30,15 @@ type Contents struct { type DRSObject struct { Id string `json:"id"` Name string `json:"name"` - SelfURL string `json:"self_url"` + SelfURL string `json:"self_url,omitempty"` Size int64 `json:"size"` - CreatedTime string `json:"created_time"` - UpdatedTime string `json:"updated_time"` - Version string `json:"version"` - MimeType string `json:"mime_type"` - Checksums []Checksum `json:"checksum"` + CreatedTime string `json:"created_time,omitempty"` + UpdatedTime string `json:"updated_time,omitempty"` + Version string `json:"version,omitempty"` + MimeType string `json:"mime_type,omitempty"` + Checksums []Checksum `json:"checksums"` AccessMethods []AccessMethod `json:"access_methods"` - Contents []Contents `json:"contents"` - Description string `json:"description"` - Aliases []string `json:"aliases"` + Contents []Contents `json:"contents,omitempty"` + Description string `json:"description,omitempty"` + Aliases []string `json:"aliases,omitempty"` } From c3c70a3e60309933148c15d46331acdfb19bf75c Mon Sep 17 00:00:00 2001 From: Kyle Ellrott Date: Mon, 12 May 2025 14:22:06 -0700 Subject: [PATCH 17/19] Starting to outline the DRS/indexd client support --- client/config.go | 58 ++++++++++++++++++++++++++++++++++++++++++++ client/indexd.go | 55 +++++++++++++++++++++++++++++++++++++++++ client/interface.go | 14 +++++++++++ cmd/add/main.go | 1 - cmd/download/main.go | 37 ++++++++++++++++++++++++++++ cmd/query/main.go | 16 ++++++++---- cmd/register/main.go | 34 ++++++++++++++++++++++++++ cmd/root.go | 4 +++ drs/client.go | 43 -------------------------------- go.mod | 25 +++++-------------- go.sum | 26 +++++++------------- 11 files changed, 228 insertions(+), 85 deletions(-) create mode 100644 client/config.go create mode 100644 client/indexd.go create mode 100644 client/interface.go create mode 100644 cmd/download/main.go create mode 100644 cmd/register/main.go delete mode 100644 drs/client.go diff --git a/client/config.go b/client/config.go new file mode 100644 index 0000000..08be1e4 --- /dev/null +++ b/client/config.go @@ -0,0 +1,58 @@ +package client + +import ( + "io" + "log" + "os" + "path/filepath" + + "github.com/bmeg/git-drs/utils" + "sigs.k8s.io/yaml" +) + +type Server struct { + BaseURL string `json:"baseURL"` + ExtensionType string `json:"type,omitempty"` +} + +type Config struct { + QueryServer Server `json:"queryServer"` + WriteServer Server `json:"writeServer"` +} + +const ( + DRS_CONFIG = ".drsconfig" +) + +func LoadConfig() (*Config, error) { + //look in Git base dir and find .drsconfig file + + topLevel, err := utils.GitTopLevel() + + if err != nil { + return nil, err + } + + configPath := filepath.Join(topLevel, DRS_CONFIG) + + log.Printf("Looking for %s", configPath) + //check if config exists + reader, err := os.Open(configPath) + if err != nil { + return nil, err + } + + b, err := io.ReadAll(reader) + if err != nil { + return nil, err + } + + conf := Config{} + err = yaml.Unmarshal(b, &conf) + if err != nil { + return nil, err + } + + log.Printf("Config: %s %#v", string(b), conf) + return &conf, nil +} diff --git a/client/indexd.go b/client/indexd.go new file mode 100644 index 0000000..d0e90a2 --- /dev/null +++ b/client/indexd.go @@ -0,0 +1,55 @@ +package client + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "path/filepath" + + "github.com/bmeg/git-drs/drs" +) + +type IndexDClient struct { + base *url.URL +} + +func NewIndexDClient(base string) (ObjectStoreClient, error) { + baseURL, err := url.Parse(base) + return &IndexDClient{baseURL}, err +} + +// DownloadFile implements ObjectStoreClient. +func (cl *IndexDClient) DownloadFile(id string, dstPath string) (*drs.DRSObject, error) { + panic("unimplemented") +} + +// RegisterFile implements ObjectStoreClient. +func (cl *IndexDClient) RegisterFile(path string, name string) (*drs.DRSObject, error) { + panic("unimplemented") +} + +func (cl *IndexDClient) QueryID(id string) (*drs.DRSObject, error) { + + a := *cl.base + a.Path = filepath.Join(a.Path, "drs/v1/objects", id) + + response, err := http.Get(a.String()) + if err != nil { + return nil, err + } + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + //log.Printf("Getting URL %s\n", a.String()) + //fmt.Printf("%s\n", string(body)) + + out := drs.DRSObject{} + err = json.Unmarshal(body, &out) + if err != nil { + return nil, err + } + return &out, nil +} diff --git a/client/interface.go b/client/interface.go new file mode 100644 index 0000000..97cce01 --- /dev/null +++ b/client/interface.go @@ -0,0 +1,14 @@ +package client + +import "github.com/bmeg/git-drs/drs" + +type ObjectStoreClient interface { + //Given a DRS string ID, retrieve the object describing it + QueryID(id string) (*drs.DRSObject, error) + + //Put file into object storage and obtain a DRS record pointing to it + RegisterFile(path string, name string) (*drs.DRSObject, error) + + //Download file given a DRS ID + DownloadFile(id string, dstPath string) (*drs.DRSObject, error) +} diff --git a/cmd/add/main.go b/cmd/add/main.go index 3ea7201..0a63c26 100644 --- a/cmd/add/main.go +++ b/cmd/add/main.go @@ -18,7 +18,6 @@ var Cmd = &cobra.Command{ matches, err := filepath.Glob(fileArg) if err == nil { for _, f := range matches { - fmt.Printf("Adding %s\n", f) } } diff --git a/cmd/download/main.go b/cmd/download/main.go new file mode 100644 index 0000000..b9b8972 --- /dev/null +++ b/cmd/download/main.go @@ -0,0 +1,37 @@ +package download + +import ( + "encoding/json" + "fmt" + + "github.com/bmeg/git-drs/client" + "github.com/spf13/cobra" +) + +var server string = "https://calypr.ohsu.edu/ga4gh" + +// Cmd line declaration +var Cmd = &cobra.Command{ + Use: "download", + Short: "Query server for DRS ID", + Long: ``, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + + client, err := client.NewIndexDClient(server) + if err != nil { + return err + } + + obj, err := client.QueryID(args[0]) + if err != nil { + return err + } + out, err := json.MarshalIndent(*obj, "", " ") + if err != nil { + return err + } + fmt.Printf("%s\n", string(out)) + return nil + }, +} diff --git a/cmd/query/main.go b/cmd/query/main.go index 189a29d..8aef738 100644 --- a/cmd/query/main.go +++ b/cmd/query/main.go @@ -4,12 +4,10 @@ import ( "encoding/json" "fmt" - "github.com/bmeg/git-drs/drs" + "github.com/bmeg/git-drs/client" "github.com/spf13/cobra" ) -var server string = "https://calypr.ohsu.edu/ga4gh" - // Cmd line declaration var Cmd = &cobra.Command{ Use: "query", @@ -18,12 +16,20 @@ var Cmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - client, err := drs.NewClient(server) + cfg, err := client.LoadConfig() + if err != nil { + return err + } + + //fix this later + baseURL := cfg.QueryServer.BaseURL + + client, err := client.NewIndexDClient(baseURL) if err != nil { return err } - obj, err := client.GetObject(args[0]) + obj, err := client.QueryID(args[0]) if err != nil { return err } diff --git a/cmd/register/main.go b/cmd/register/main.go new file mode 100644 index 0000000..2f8bd41 --- /dev/null +++ b/cmd/register/main.go @@ -0,0 +1,34 @@ +package register + +import ( + "log" + "path/filepath" + + "github.com/bmeg/git-drs/client" + "github.com/spf13/cobra" +) + +var server string = "https://calypr.ohsu.edu/ga4gh" + +// Cmd line declaration +var Cmd = &cobra.Command{ + Use: "register", + Short: "", + Long: ``, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + log.Printf("Registering file %s", args[0]) + client, err := client.NewIndexDClient(server) + if err != nil { + return err + } + + //upload the file, name would probably be relative to the base of the git repo + client.RegisterFile(args[0], filepath.Base(args[0])) + + //remove later + _ = client + + return nil + }, +} diff --git a/cmd/root.go b/cmd/root.go index e0b2b56..cb463eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,12 +4,14 @@ import ( "os" "github.com/bmeg/git-drs/cmd/add" + "github.com/bmeg/git-drs/cmd/download" "github.com/bmeg/git-drs/cmd/filterprocess" "github.com/bmeg/git-drs/cmd/initialize" "github.com/bmeg/git-drs/cmd/list" "github.com/bmeg/git-drs/cmd/pull" "github.com/bmeg/git-drs/cmd/push" "github.com/bmeg/git-drs/cmd/query" + "github.com/bmeg/git-drs/cmd/register" "github.com/spf13/cobra" ) @@ -31,6 +33,8 @@ func init() { RootCmd.AddCommand(add.Cmd) RootCmd.AddCommand(filterprocess.Cmd) RootCmd.AddCommand(query.Cmd) + RootCmd.AddCommand(register.Cmd) + RootCmd.AddCommand(download.Cmd) RootCmd.AddCommand(genBashCompletionCmd) } diff --git a/drs/client.go b/drs/client.go deleted file mode 100644 index 0b7bac7..0000000 --- a/drs/client.go +++ /dev/null @@ -1,43 +0,0 @@ -package drs - -import ( - "encoding/json" - "io" - "net/http" - "net/url" - "path/filepath" -) - -type Client struct { - base *url.URL -} - -func NewClient(base string) (*Client, error) { - baseURL, err := url.Parse(base) - return &Client{baseURL}, err -} - -func (cl *Client) GetObject(id string) (*DRSObject, error) { - - a := *cl.base - a.Path = filepath.Join(a.Path, "drs/v1/objects", id) - - response, err := http.Get(a.String()) - if err != nil { - return nil, err - } - - body, err := io.ReadAll(response.Body) - if err != nil { - return nil, err - } - //log.Printf("Getting URL %s\n", a.String()) - //fmt.Printf("%s\n", string(body)) - - out := DRSObject{} - err = json.Unmarshal(body, &out) - if err != nil { - return nil, err - } - return &out, nil -} diff --git a/go.mod b/go.mod index 4e11a3f..c58f9ee 100644 --- a/go.mod +++ b/go.mod @@ -3,34 +3,21 @@ module github.com/bmeg/git-drs go 1.24.0 require ( - github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect + github.com/git-lfs/git-lfs/v3 v3.6.1 + github.com/spf13/cobra v1.9.1 + sigs.k8s.io/yaml v1.4.0 +) + +require ( github.com/avast/retry-go v2.4.2+incompatible // indirect - github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430 // indirect - github.com/git-lfs/git-lfs/v3 v3.6.1 // indirect github.com/git-lfs/gitobj/v2 v2.1.1 // indirect - github.com/git-lfs/go-netrc v0.0.0-20210914205454-f0c862dd687a // indirect github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 // indirect github.com/git-lfs/wildmatch/v2 v2.0.1 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jcmturner/aescts/v2 v2.0.0 // indirect - github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect - github.com/jcmturner/gofork v1.0.0 // indirect - github.com/jcmturner/goidentity/v6 v6.0.1 // indirect - github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect - github.com/jcmturner/rpc/v2 v2.0.3 // indirect - github.com/jmhodges/clock v1.2.0 // indirect github.com/leonelquinteros/gotext v1.5.0 // indirect - github.com/mattn/go-isatty v0.0.4 // indirect - github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 // indirect github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 // indirect github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 // indirect - github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/ssgelm/cookiejarparser v1.0.1 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index f79b8f3..7e48ed3 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,7 @@ github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1L github.com/avast/retry-go v2.4.2+incompatible h1:+ZjCypQT/CyP0kyJO2EcU4d/ZEJWSbP8NENI578cPmA= github.com/avast/retry-go v2.4.2+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430 h1:oempk9HjNt6rVKyKmpdnoN7XABQv3SXLWu3pxUI7Vlk= github.com/dpotapov/go-spnego v0.0.0-20210315154721-298b63a54430/go.mod h1:AVSs/gZKt1bOd2AhkhbS7Qh56Hv7klde22yXVbwYJhc= @@ -17,8 +17,8 @@ github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 h1:riQhgheTL7tMF4d github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= github.com/git-lfs/wildmatch/v2 v2.0.1 h1:Ds+aobrV5bK0wStILUOn9irllPyf9qrFETbKzwzoER8= github.com/git-lfs/wildmatch/v2 v2.0.1/go.mod h1:EVqonpk9mXbREP3N8UkwoWdrF249uHpCUo5CPXY81gw= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -45,6 +45,7 @@ github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2 github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 h1:chPfVn+gpAM5CTpTyVU9j8J+xgRGwmoDlNDLjKnJiYo= github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0= github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4= @@ -55,40 +56,31 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/ssgelm/cookiejarparser v1.0.1 h1:cRdXauUbOTFzTPJFaeiWbHnQ+tRGlpKKzvIK9PUekE4= github.com/ssgelm/cookiejarparser v1.0.1/go.mod h1:DUfC0mpjIzlDN7DzKjXpHj0qMI5m9VrZuz3wSlI+OEI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191027093000-83d349e8ac1a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 317750b2f930193350fa558589072326975292d9 Mon Sep 17 00:00:00 2001 From: quinnwai Date: Wed, 14 May 2025 14:00:34 -0700 Subject: [PATCH 18/19] 1st draft DRS query and download, make sure to setup .drsconfig --- .drsconfig | 9 ++++ client/config.go | 1 + client/indexd.go | 106 +++++++++++++++++++++++++++++++++++++++++-- client/interface.go | 2 +- cmd/download/main.go | 13 ++++-- go.mod | 13 ++++++ go.sum | 20 ++++++++ 7 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 .drsconfig diff --git a/.drsconfig b/.drsconfig new file mode 100644 index 0000000..13553b9 --- /dev/null +++ b/.drsconfig @@ -0,0 +1,9 @@ +{ + "queryServer": { + "baseURL": "https://calypr.ohsu.edu/ga4gh" + }, + "writeServer": { + "baseURL": "https://calypr.ohsu.edu/ga4gh" + }, + "gen3Profile": "" +} diff --git a/client/config.go b/client/config.go index 08be1e4..12845db 100644 --- a/client/config.go +++ b/client/config.go @@ -18,6 +18,7 @@ type Server struct { type Config struct { QueryServer Server `json:"queryServer"` WriteServer Server `json:"writeServer"` + Gen3Profile string `json:"gen3Profile"` } const ( diff --git a/client/indexd.go b/client/indexd.go index d0e90a2..e6694f5 100644 --- a/client/indexd.go +++ b/client/indexd.go @@ -2,26 +2,116 @@ package client import ( "encoding/json" + "fmt" "io" "net/http" "net/url" + "os" "path/filepath" "github.com/bmeg/git-drs/drs" + "github.com/uc-cdis/gen3-client/gen3-client/jwt" ) +var conf jwt.Configure +var profileConfig jwt.Credential + type IndexDClient struct { base *url.URL } func NewIndexDClient(base string) (ObjectStoreClient, error) { baseURL, err := url.Parse(base) + // print baseURL + if err != nil { + return nil, err + } + fmt.Printf("Base URL: %s\n", baseURL.String()) + return &IndexDClient{baseURL}, err } // DownloadFile implements ObjectStoreClient. -func (cl *IndexDClient) DownloadFile(id string, dstPath string) (*drs.DRSObject, error) { - panic("unimplemented") +func (cl *IndexDClient) DownloadFile(id string, access_id string, profile string, dstPath string) (*drs.AccessURL, error) { + + // get file from indexd + a := *cl.base + a.Path = filepath.Join(a.Path, "drs/v1/objects", id, "access", access_id) + // a.Path = filepath.Join("https://calypr.ohsu.edu/user/data/download/", id) + + fmt.Print("Getting URL: ", a.String(), "\n") + + // unmarshal response + req, err := http.NewRequest("GET", a.String(), nil) + if err != nil { + return nil, err + } + // extract accessToken from config and insert into header of request + profileConfig = conf.ParseConfig(profile) + if profileConfig.AccessToken == "" { + return nil, fmt.Errorf("access token not found in profile config") + } + + // Add headers to the request + authStr := fmt.Sprintf("Bearer %s", profileConfig.AccessToken) + fmt.Printf("Authorization header: %s\n", authStr) + req.Header.Set("Authorization", authStr) + + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + return nil, err + } + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + // print body + fmt.Printf("Response body: %s\n", string(body)) + + out := drs.AccessURL{} + err = json.Unmarshal(body, &out) + if err != nil { + return nil, err + } + + // Extract the signed URL from the response + signedURL := out.URL // Assuming `out.url` contains the signed URL + if signedURL == "" { + return nil, fmt.Errorf("signed URL not found in response") + } + + fmt.Print("Signed URL: ", signedURL, "\n") + + // Download the file using the signed URL + fileResponse, err := http.Get(signedURL) + if err != nil { + return nil, err + } + defer fileResponse.Body.Close() + + fmt.Printf("File response status: %s\n", fileResponse.Status) + + // Create the destination file + dstFile, err := os.Create(dstPath) + if err != nil { + return nil, err + } + defer dstFile.Close() + + // print file response as string + fmt.Printf("File response contents: %s\n", fileResponse.Body) + + // Write the file content to the destination file + _, err = io.Copy(dstFile, fileResponse.Body) + if err != nil { + return nil, err + } + + return &out, nil } // RegisterFile implements ObjectStoreClient. @@ -34,10 +124,20 @@ func (cl *IndexDClient) QueryID(id string) (*drs.DRSObject, error) { a := *cl.base a.Path = filepath.Join(a.Path, "drs/v1/objects", id) - response, err := http.Get(a.String()) + req, err := http.NewRequest("GET", a.String(), nil) + if err != nil { + return nil, err + } + // Add headers to the request + req.Header.Set("Authorization", "Bearer ") + req.Header.Set("Custom-Header", "HeaderValue") + + client := &http.Client{} + response, err := client.Do(req) if err != nil { return nil, err } + defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { diff --git a/client/interface.go b/client/interface.go index 97cce01..208e779 100644 --- a/client/interface.go +++ b/client/interface.go @@ -10,5 +10,5 @@ type ObjectStoreClient interface { RegisterFile(path string, name string) (*drs.DRSObject, error) //Download file given a DRS ID - DownloadFile(id string, dstPath string) (*drs.DRSObject, error) + DownloadFile(id string, access_id string, profile string, dstPath string) (*drs.AccessURL, error) } diff --git a/cmd/download/main.go b/cmd/download/main.go index b9b8972..4ce4d80 100644 --- a/cmd/download/main.go +++ b/cmd/download/main.go @@ -13,7 +13,7 @@ var server string = "https://calypr.ohsu.edu/ga4gh" // Cmd line declaration var Cmd = &cobra.Command{ Use: "download", - Short: "Query server for DRS ID", + Short: "Download file using s3", Long: ``, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -23,11 +23,18 @@ var Cmd = &cobra.Command{ return err } - obj, err := client.QueryID(args[0]) + // // get file name from DRS object + // drs_obj, err := client.QueryID(args[0]) + // if err != nil { + // return err + // } + + access_url, err := client.DownloadFile(args[0], "s3", "cbds-prod", "./file.txt") if err != nil { return err } - out, err := json.MarshalIndent(*obj, "", " ") + + out, err := json.MarshalIndent(*access_url, "", " ") if err != nil { return err } diff --git a/go.mod b/go.mod index c58f9ee..1743ff5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.0 require ( github.com/git-lfs/git-lfs/v3 v3.6.1 github.com/spf13/cobra v1.9.1 + github.com/uc-cdis/gen3-client v0.0.23 sigs.k8s.io/yaml v1.4.0 ) @@ -13,11 +14,23 @@ require ( github.com/git-lfs/gitobj/v2 v2.1.1 // indirect github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 // indirect github.com/git-lfs/wildmatch/v2 v2.0.1 // indirect + github.com/google/go-github v17.0.0+incompatible // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/hashicorp/go-version v1.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/leonelquinteros/gotext v1.5.0 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 // indirect + github.com/rivo/uniseg v0.2.0 // indirect github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 // indirect github.com/spf13/pflag v1.0.6 // indirect + github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect + gopkg.in/ini.v1 v1.66.3 // indirect ) + +replace github.com/uc-cdis/gen3-client => ../cdis-data-client diff --git a/go.sum b/go.sum index 7e48ed3..e94a1a5 100644 --- a/go.sum +++ b/go.sum @@ -17,10 +17,17 @@ github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825 h1:riQhgheTL7tMF4d github.com/git-lfs/pktline v0.0.0-20210330133718-06e9096e2825/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= github.com/git-lfs/wildmatch/v2 v2.0.1 h1:Ds+aobrV5bK0wStILUOn9irllPyf9qrFETbKzwzoER8= github.com/git-lfs/wildmatch/v2 v2.0.1/go.mod h1:EVqonpk9mXbREP3N8UkwoWdrF249uHpCUo5CPXY81gw= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= +github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= @@ -41,12 +48,18 @@ github.com/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914F github.com/leonelquinteros/gotext v1.5.0/go.mod h1:OCiUVHuhP9LGFBQ1oAmdtNCHJCiHiQA8lf4nAifHkr0= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw= github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17 h1:chPfVn+gpAM5CTpTyVU9j8J+xgRGwmoDlNDLjKnJiYo= github.com/pkg/errors v0.0.0-20170505043639-c605e284fe17/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086 h1:mncRSDOqYCng7jOD+Y6+IivdRI6Kzv2BLWYkWkdQfu0= github.com/rubyist/tracerx v0.0.0-20170927163412-787959303086/go.mod h1:YpdgDXpumPB/+EGmGTYHeiW/0QVFRzBYTNFaxWfPDk4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -59,6 +72,8 @@ github.com/ssgelm/cookiejarparser v1.0.1/go.mod h1:DUfC0mpjIzlDN7DzKjXpHj0qMI5m9 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k= +github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= @@ -78,8 +93,13 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/ini.v1 v1.66.3 h1:jRskFVxYaMGAMUbN0UZ7niA9gzL9B49DOqE78vg0k3w= +gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= From 90f6b63696d72c42011c0165d8cdedc0a6b26a35 Mon Sep 17 00:00:00 2001 From: quinnwai Date: Wed, 14 May 2025 17:07:18 -0700 Subject: [PATCH 19/19] make DownloadFile more loosely coupled --- .gitignore | 1 + client/README.md | 14 +++++++++++ client/indexd.go | 59 ++++++++++++++++++++++++++------------------ client/interface.go | 2 +- cmd/download/main.go | 49 ++++++++++++++++++++++-------------- cmd/query/main.go | 6 ++--- 6 files changed, 85 insertions(+), 46 deletions(-) create mode 100644 client/README.md diff --git a/.gitignore b/.gitignore index f32e31a..0088ced 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea/ .DS_Store +/tmp \ No newline at end of file diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..45e7cf8 --- /dev/null +++ b/client/README.md @@ -0,0 +1,14 @@ +# Git DRS Client + +## Getting Started + +1. Configure gen3 with your credentials ([docs](https://aced-idp.github.io/requirements/#1-download-gen3-client)) +2. Edit platform URL and gen3 profile in `.drsconfig` +3. Build from source + ```bash + go build + ``` +4. Access through command line + ```bash + ./git-drs --help + ``` diff --git a/client/indexd.go b/client/indexd.go index e6694f5..1d0a592 100644 --- a/client/indexd.go +++ b/client/indexd.go @@ -17,7 +17,8 @@ var conf jwt.Configure var profileConfig jwt.Credential type IndexDClient struct { - base *url.URL + base *url.URL + profile string } func NewIndexDClient(base string) (ObjectStoreClient, error) { @@ -26,35 +27,44 @@ func NewIndexDClient(base string) (ObjectStoreClient, error) { if err != nil { return nil, err } + + cfg, err := LoadConfig() + if err != nil { + return nil, err + } + + // get the gen3Profile from the config + profile := cfg.Gen3Profile + if profile == "" { + return nil, fmt.Errorf("No gen3 profile specified. Please provide a gen3Profile key in your .drsconfig") + } + fmt.Printf("Base URL: %s\n", baseURL.String()) + fmt.Printf("Profile: %s\n", profile) - return &IndexDClient{baseURL}, err + return &IndexDClient{baseURL, profile}, err } -// DownloadFile implements ObjectStoreClient. -func (cl *IndexDClient) DownloadFile(id string, access_id string, profile string, dstPath string) (*drs.AccessURL, error) { - +// DownloadFile implements ObjectStoreClient +func (cl *IndexDClient) DownloadFile(id string, access_id string, dstPath string) (*drs.AccessURL, error) { // get file from indexd a := *cl.base a.Path = filepath.Join(a.Path, "drs/v1/objects", id, "access", access_id) // a.Path = filepath.Join("https://calypr.ohsu.edu/user/data/download/", id) - fmt.Print("Getting URL: ", a.String(), "\n") - // unmarshal response req, err := http.NewRequest("GET", a.String(), nil) if err != nil { return nil, err } - // extract accessToken from config and insert into header of request - profileConfig = conf.ParseConfig(profile) + // extract accessToken from gen3 profile and insert into header of request + profileConfig = conf.ParseConfig(cl.profile) if profileConfig.AccessToken == "" { return nil, fmt.Errorf("access token not found in profile config") } // Add headers to the request - authStr := fmt.Sprintf("Bearer %s", profileConfig.AccessToken) - fmt.Printf("Authorization header: %s\n", authStr) + authStr := "Bearer " + profileConfig.AccessToken req.Header.Set("Authorization", authStr) client := &http.Client{} @@ -69,9 +79,6 @@ func (cl *IndexDClient) DownloadFile(id string, access_id string, profile string return nil, err } - // print body - fmt.Printf("Response body: %s\n", string(body)) - out := drs.AccessURL{} err = json.Unmarshal(body, &out) if err != nil { @@ -79,13 +86,11 @@ func (cl *IndexDClient) DownloadFile(id string, access_id string, profile string } // Extract the signed URL from the response - signedURL := out.URL // Assuming `out.url` contains the signed URL + signedURL := out.URL if signedURL == "" { - return nil, fmt.Errorf("signed URL not found in response") + return nil, fmt.Errorf("signed URL not found in response.") } - fmt.Print("Signed URL: ", signedURL, "\n") - // Download the file using the signed URL fileResponse, err := http.Get(signedURL) if err != nil { @@ -93,7 +98,16 @@ func (cl *IndexDClient) DownloadFile(id string, access_id string, profile string } defer fileResponse.Body.Close() - fmt.Printf("File response status: %s\n", fileResponse.Status) + // Check if the response status is OK + if fileResponse.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to download file using signed URL: %s", fileResponse.Status) + } + + // Create the destination directory if it doesn't exist + err = os.MkdirAll(filepath.Dir(dstPath), os.ModePerm) + if err != nil { + return nil, err + } // Create the destination file dstFile, err := os.Create(dstPath) @@ -102,15 +116,14 @@ func (cl *IndexDClient) DownloadFile(id string, access_id string, profile string } defer dstFile.Close() - // print file response as string - fmt.Printf("File response contents: %s\n", fileResponse.Body) - // Write the file content to the destination file _, err = io.Copy(dstFile, fileResponse.Body) if err != nil { return nil, err } + fmt.Printf("File written to %s\n", dstFile.Name()) + return &out, nil } @@ -143,8 +156,6 @@ func (cl *IndexDClient) QueryID(id string) (*drs.DRSObject, error) { if err != nil { return nil, err } - //log.Printf("Getting URL %s\n", a.String()) - //fmt.Printf("%s\n", string(body)) out := drs.DRSObject{} err = json.Unmarshal(body, &out) diff --git a/client/interface.go b/client/interface.go index 208e779..8b6bba1 100644 --- a/client/interface.go +++ b/client/interface.go @@ -10,5 +10,5 @@ type ObjectStoreClient interface { RegisterFile(path string, name string) (*drs.DRSObject, error) //Download file given a DRS ID - DownloadFile(id string, access_id string, profile string, dstPath string) (*drs.AccessURL, error) + DownloadFile(id string, access_id string, dstPath string) (*drs.AccessURL, error) } diff --git a/cmd/download/main.go b/cmd/download/main.go index 4ce4d80..067d642 100644 --- a/cmd/download/main.go +++ b/cmd/download/main.go @@ -1,44 +1,57 @@ package download import ( - "encoding/json" - "fmt" - "github.com/bmeg/git-drs/client" + "github.com/bmeg/git-drs/drs" "github.com/spf13/cobra" ) -var server string = "https://calypr.ohsu.edu/ga4gh" +var ( + server string + dstPath string + drsObj *drs.DRSObject +) +// Cmd line declaration // Cmd line declaration var Cmd = &cobra.Command{ - Use: "download", - Short: "Download file using s3", - Long: ``, - Args: cobra.MinimumNArgs(1), + Use: "download ", + Short: "Download file using DRS ID and access ID", + Long: "Download file using DRS ID and access ID. The access ID is the access method used to download the file.", + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - - client, err := client.NewIndexDClient(server) + drsId := args[0] + accessId := args[1] + cfg, err := client.LoadConfig() if err != nil { return err } - // // get file name from DRS object - // drs_obj, err := client.QueryID(args[0]) - // if err != nil { - // return err - // } + baseURL := cfg.QueryServer.BaseURL - access_url, err := client.DownloadFile(args[0], "s3", "cbds-prod", "./file.txt") + client, err := client.NewIndexDClient(baseURL) if err != nil { return err } - out, err := json.MarshalIndent(*access_url, "", " ") + if dstPath == "" { + + drsObj, err = client.QueryID(drsId) + if err != nil { + return err + } + dstPath = drsObj.Name + } + + _, err = client.DownloadFile(drsId, accessId, dstPath) if err != nil { return err } - fmt.Printf("%s\n", string(out)) + return nil }, } + +func init() { + Cmd.Flags().StringVarP(&dstPath, "dstPath", "d", "", "Optional destination file path") +} diff --git a/cmd/query/main.go b/cmd/query/main.go index 8aef738..b9463d7 100644 --- a/cmd/query/main.go +++ b/cmd/query/main.go @@ -10,9 +10,9 @@ import ( // Cmd line declaration var Cmd = &cobra.Command{ - Use: "query", - Short: "Query server for DRS ID", - Long: ``, + Use: "query ", + Short: "Query DRS server by DRS ID", + Long: "Query DRS server by DRS ID", Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error {