pydorky 2.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/devcontainer.json +17 -0
- package/.github/FUNDING.yml +15 -0
- package/.github/workflows/e2e-integration.yml +57 -0
- package/.github/workflows/publish.yml +24 -0
- package/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/index.js +19 -0
- package/bin/legacy.js +432 -0
- package/docs/doc#1 get-started/README.md +105 -0
- package/docs/doc#2 features-wishlist +33 -0
- package/docs/doc#2.5 python-port +31 -0
- package/docs/doc#3 the-correct-node-version +107 -0
- package/docs/doc#4 why-where-python +42 -0
- package/docs/doc#5 how-do-endpoints-cli-work +0 -0
- package/dorky-usage-aws.svg +1 -0
- package/dorky-usage-google-drive.svg +1 -0
- package/google-drive-credentials.json +16 -0
- package/openapi/openapi.yaml +257 -0
- package/package.json +46 -0
- package/python-client/README.md +19 -0
- package/python-client/dorky_client/__init__.py +3 -0
- package/python-client/dorky_client/client.py +32 -0
- package/python-client/pyproject.toml +13 -0
- package/python-client/tests/test_integration.py +20 -0
- package/rectdorky.png +0 -0
- package/server/index.js +193 -0
- package/server/package.json +12 -0
- package/todo/01-core-infrastructure.md +84 -0
- package/todo/02-storage-providers.md +104 -0
- package/todo/03-compression-formats.md +94 -0
- package/todo/04-python-client.md +126 -0
- package/todo/05-metadata-versioning.md +116 -0
- package/todo/06-performance-concurrency.md +130 -0
- package/todo/07-security-encryption.md +114 -0
- package/todo/08-developer-experience.md +175 -0
- package/todo/README.md +37 -0
- package/web-app/README.md +70 -0
- package/web-app/package-lock.json +17915 -0
- package/web-app/package.json +43 -0
- package/web-app/public/favicon.ico +0 -0
- package/web-app/public/index.html +43 -0
- package/web-app/public/logo192.png +0 -0
- package/web-app/public/logo512.png +0 -0
- package/web-app/public/manifest.json +25 -0
- package/web-app/public/robots.txt +3 -0
- package/web-app/src/App.css +23 -0
- package/web-app/src/App.js +84 -0
- package/web-app/src/App.test.js +8 -0
- package/web-app/src/PrivacyPolicy.js +26 -0
- package/web-app/src/TermsAndConditions.js +41 -0
- package/web-app/src/index.css +3 -0
- package/web-app/src/index.js +26 -0
- package/web-app/src/logo.svg +1 -0
- package/web-app/src/reportWebVitals.js +13 -0
- package/web-app/src/setupTests.js +5 -0
- package/web-app/tailwind.config.js +10 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"web": {
|
|
3
|
+
"client_id": "549624816041-j9tr8es6megehe18i0c04vmpno6q78ld.apps.googleusercontent.com",
|
|
4
|
+
"project_id": "dorky-445907",
|
|
5
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
6
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
7
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
8
|
+
"client_secret": "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",
|
|
9
|
+
"redirect_uris": [
|
|
10
|
+
"http://localhost:3000/oauth2callback"
|
|
11
|
+
],
|
|
12
|
+
"javascript_origins": [
|
|
13
|
+
"http://localhost:3000"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
openapi: 3.0.1
|
|
2
|
+
info:
|
|
3
|
+
title: Pydorky Artifact Service
|
|
4
|
+
description: Language-agnostic HTTP service for artifact storage, streaming, metadata, idempotency, and versioning
|
|
5
|
+
version: 0.2.0
|
|
6
|
+
servers:
|
|
7
|
+
- url: http://localhost:3000
|
|
8
|
+
description: Local development server
|
|
9
|
+
|
|
10
|
+
paths:
|
|
11
|
+
/health:
|
|
12
|
+
get:
|
|
13
|
+
summary: Health check
|
|
14
|
+
operationId: health
|
|
15
|
+
tags:
|
|
16
|
+
- service
|
|
17
|
+
responses:
|
|
18
|
+
'200':
|
|
19
|
+
description: Service is healthy
|
|
20
|
+
content:
|
|
21
|
+
application/json:
|
|
22
|
+
schema:
|
|
23
|
+
type: object
|
|
24
|
+
properties:
|
|
25
|
+
status:
|
|
26
|
+
type: string
|
|
27
|
+
example: healthy
|
|
28
|
+
timestamp:
|
|
29
|
+
type: string
|
|
30
|
+
format: date-time
|
|
31
|
+
|
|
32
|
+
/artifacts:
|
|
33
|
+
post:
|
|
34
|
+
summary: Upload an artifact
|
|
35
|
+
operationId: uploadArtifact
|
|
36
|
+
tags:
|
|
37
|
+
- artifacts
|
|
38
|
+
requestBody:
|
|
39
|
+
required: true
|
|
40
|
+
content:
|
|
41
|
+
multipart/form-data:
|
|
42
|
+
schema:
|
|
43
|
+
type: object
|
|
44
|
+
properties:
|
|
45
|
+
file:
|
|
46
|
+
type: string
|
|
47
|
+
format: binary
|
|
48
|
+
description: The file to upload
|
|
49
|
+
metadata:
|
|
50
|
+
type: string
|
|
51
|
+
description: JSON metadata as string
|
|
52
|
+
idempotency_key:
|
|
53
|
+
type: string
|
|
54
|
+
description: Unique key to prevent duplicate uploads
|
|
55
|
+
required:
|
|
56
|
+
- file
|
|
57
|
+
responses:
|
|
58
|
+
'201':
|
|
59
|
+
description: Artifact created successfully
|
|
60
|
+
content:
|
|
61
|
+
application/json:
|
|
62
|
+
schema:
|
|
63
|
+
$ref: '#/components/schemas/Artifact'
|
|
64
|
+
'400':
|
|
65
|
+
description: Bad request (missing file, invalid metadata)
|
|
66
|
+
'409':
|
|
67
|
+
description: Conflict (artifact already exists with same idempotency key)
|
|
68
|
+
|
|
69
|
+
/artifacts/{id}:
|
|
70
|
+
get:
|
|
71
|
+
summary: Download an artifact
|
|
72
|
+
operationId: downloadArtifact
|
|
73
|
+
tags:
|
|
74
|
+
- artifacts
|
|
75
|
+
parameters:
|
|
76
|
+
- name: id
|
|
77
|
+
in: path
|
|
78
|
+
required: true
|
|
79
|
+
schema:
|
|
80
|
+
type: string
|
|
81
|
+
description: Artifact ID
|
|
82
|
+
- name: version
|
|
83
|
+
in: query
|
|
84
|
+
required: false
|
|
85
|
+
schema:
|
|
86
|
+
type: string
|
|
87
|
+
description: Specific version to download (optional)
|
|
88
|
+
responses:
|
|
89
|
+
'200':
|
|
90
|
+
description: Artifact stream
|
|
91
|
+
content:
|
|
92
|
+
application/octet-stream: {}
|
|
93
|
+
'404':
|
|
94
|
+
description: Artifact not found
|
|
95
|
+
'416':
|
|
96
|
+
description: Range not satisfiable
|
|
97
|
+
|
|
98
|
+
delete:
|
|
99
|
+
summary: Delete an artifact
|
|
100
|
+
operationId: deleteArtifact
|
|
101
|
+
tags:
|
|
102
|
+
- artifacts
|
|
103
|
+
parameters:
|
|
104
|
+
- name: id
|
|
105
|
+
in: path
|
|
106
|
+
required: true
|
|
107
|
+
schema:
|
|
108
|
+
type: string
|
|
109
|
+
responses:
|
|
110
|
+
'204':
|
|
111
|
+
description: Artifact deleted successfully
|
|
112
|
+
'404':
|
|
113
|
+
description: Artifact not found
|
|
114
|
+
|
|
115
|
+
/artifacts/{id}/metadata:
|
|
116
|
+
get:
|
|
117
|
+
summary: Get artifact metadata
|
|
118
|
+
operationId: getMetadata
|
|
119
|
+
tags:
|
|
120
|
+
- metadata
|
|
121
|
+
parameters:
|
|
122
|
+
- name: id
|
|
123
|
+
in: path
|
|
124
|
+
required: true
|
|
125
|
+
schema:
|
|
126
|
+
type: string
|
|
127
|
+
responses:
|
|
128
|
+
'200':
|
|
129
|
+
description: Artifact metadata
|
|
130
|
+
content:
|
|
131
|
+
application/json:
|
|
132
|
+
schema:
|
|
133
|
+
$ref: '#/components/schemas/Metadata'
|
|
134
|
+
'404':
|
|
135
|
+
description: Artifact not found
|
|
136
|
+
|
|
137
|
+
patch:
|
|
138
|
+
summary: Update artifact metadata
|
|
139
|
+
operationId: updateMetadata
|
|
140
|
+
tags:
|
|
141
|
+
- metadata
|
|
142
|
+
parameters:
|
|
143
|
+
- name: id
|
|
144
|
+
in: path
|
|
145
|
+
required: true
|
|
146
|
+
schema:
|
|
147
|
+
type: string
|
|
148
|
+
requestBody:
|
|
149
|
+
required: true
|
|
150
|
+
content:
|
|
151
|
+
application/json:
|
|
152
|
+
schema:
|
|
153
|
+
type: object
|
|
154
|
+
additionalProperties: true
|
|
155
|
+
responses:
|
|
156
|
+
'200':
|
|
157
|
+
description: Metadata updated
|
|
158
|
+
content:
|
|
159
|
+
application/json:
|
|
160
|
+
schema:
|
|
161
|
+
$ref: '#/components/schemas/Metadata'
|
|
162
|
+
'404':
|
|
163
|
+
description: Artifact not found
|
|
164
|
+
|
|
165
|
+
/artifacts/{id}/versions:
|
|
166
|
+
get:
|
|
167
|
+
summary: List all versions of an artifact
|
|
168
|
+
operationId: listVersions
|
|
169
|
+
tags:
|
|
170
|
+
- versioning
|
|
171
|
+
parameters:
|
|
172
|
+
- name: id
|
|
173
|
+
in: path
|
|
174
|
+
required: true
|
|
175
|
+
schema:
|
|
176
|
+
type: string
|
|
177
|
+
responses:
|
|
178
|
+
'200':
|
|
179
|
+
description: List of versions
|
|
180
|
+
content:
|
|
181
|
+
application/json:
|
|
182
|
+
schema:
|
|
183
|
+
type: array
|
|
184
|
+
items:
|
|
185
|
+
$ref: '#/components/schemas/Version'
|
|
186
|
+
'404':
|
|
187
|
+
description: Artifact not found
|
|
188
|
+
|
|
189
|
+
components:
|
|
190
|
+
schemas:
|
|
191
|
+
Artifact:
|
|
192
|
+
type: object
|
|
193
|
+
required:
|
|
194
|
+
- id
|
|
195
|
+
- url
|
|
196
|
+
properties:
|
|
197
|
+
id:
|
|
198
|
+
type: string
|
|
199
|
+
example: "1a2b3c"
|
|
200
|
+
url:
|
|
201
|
+
type: string
|
|
202
|
+
example: "/artifacts/1a2b3c"
|
|
203
|
+
metadata:
|
|
204
|
+
$ref: '#/components/schemas/Metadata'
|
|
205
|
+
version:
|
|
206
|
+
type: string
|
|
207
|
+
example: "1"
|
|
208
|
+
|
|
209
|
+
Metadata:
|
|
210
|
+
type: object
|
|
211
|
+
required:
|
|
212
|
+
- content_hash
|
|
213
|
+
- file_size
|
|
214
|
+
- last_updated
|
|
215
|
+
properties:
|
|
216
|
+
content_hash:
|
|
217
|
+
type: string
|
|
218
|
+
description: SHA256 hash of file content
|
|
219
|
+
example: "abc123def456..."
|
|
220
|
+
file_size:
|
|
221
|
+
type: integer
|
|
222
|
+
description: File size in bytes
|
|
223
|
+
example: 1024
|
|
224
|
+
compressed_size:
|
|
225
|
+
type: integer
|
|
226
|
+
description: Compressed file size (if applicable)
|
|
227
|
+
compression_type:
|
|
228
|
+
type: string
|
|
229
|
+
description: Compression algorithm used
|
|
230
|
+
enum: [gzip, lz4, brotli, null]
|
|
231
|
+
last_updated:
|
|
232
|
+
type: string
|
|
233
|
+
format: date-time
|
|
234
|
+
description: Last update timestamp (UTC ISO 8601)
|
|
235
|
+
uploaded_by:
|
|
236
|
+
type: string
|
|
237
|
+
description: User/identifier who uploaded
|
|
238
|
+
custom:
|
|
239
|
+
type: object
|
|
240
|
+
description: User-defined metadata
|
|
241
|
+
additionalProperties: true
|
|
242
|
+
|
|
243
|
+
Version:
|
|
244
|
+
type: object
|
|
245
|
+
required:
|
|
246
|
+
- version_id
|
|
247
|
+
- created_at
|
|
248
|
+
- metadata
|
|
249
|
+
properties:
|
|
250
|
+
version_id:
|
|
251
|
+
type: string
|
|
252
|
+
example: "1"
|
|
253
|
+
created_at:
|
|
254
|
+
type: string
|
|
255
|
+
format: date-time
|
|
256
|
+
metadata:
|
|
257
|
+
$ref: '#/components/schemas/Metadata'
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pydorky",
|
|
3
|
+
"version": "2.1.8",
|
|
4
|
+
"description": "DevOps Records Keeper.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"dorky": "bin/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start": "node bin/index.js",
|
|
10
|
+
"test": "mocha"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Pratham-Jain-3903/pydorky.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"DevOps",
|
|
18
|
+
"env",
|
|
19
|
+
"yaml",
|
|
20
|
+
"json"
|
|
21
|
+
],
|
|
22
|
+
"author": "Pydorky contributors",
|
|
23
|
+
"license": "ISC",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Pratham-Jain-3903/pydorky/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/Pratham-Jain-3903/pydorky#readme",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"tsc": "^2.0.4",
|
|
33
|
+
"typescript": "^5.5.4"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@aws-sdk/client-s3": "^3.679.0",
|
|
37
|
+
"@google-cloud/local-auth": "^3.0.1",
|
|
38
|
+
"chalk": "^4.1.2",
|
|
39
|
+
"glob": "^13.0.0",
|
|
40
|
+
"googleapis": "^144.0.0",
|
|
41
|
+
"md5": "^2.3.0",
|
|
42
|
+
"mime-type": "^4.0.0",
|
|
43
|
+
"mime-types": "^2.1.35",
|
|
44
|
+
"yargs": "^17.7.2"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Dorky Python client
|
|
2
|
+
|
|
3
|
+
This is a minimal scaffold for the Dorky Python client. It implements a thin wrapper
|
|
4
|
+
around the Dorky HTTP API (see `openapi/openapi.yaml`).
|
|
5
|
+
|
|
6
|
+
Install (local editable):
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install -e python-client
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Usage example:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from dorky_client import DorkyClient
|
|
16
|
+
|
|
17
|
+
client = DorkyClient('http://localhost:3000')
|
|
18
|
+
client.upload('path/to/file.txt', metadata={'note':'example'})
|
|
19
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Optional, Dict
|
|
4
|
+
|
|
5
|
+
CHUNK_SIZE = 1024 * 64
|
|
6
|
+
|
|
7
|
+
class DorkyClient:
|
|
8
|
+
def __init__(self, base_url: str):
|
|
9
|
+
self.base_url = base_url.rstrip('/')
|
|
10
|
+
|
|
11
|
+
def upload(self, file_path: str, metadata: Optional[Dict]=None, idempotency_key: Optional[str]=None):
|
|
12
|
+
url = f"{self.base_url}/artifacts"
|
|
13
|
+
files = {'file': open(file_path, 'rb')}
|
|
14
|
+
data = {}
|
|
15
|
+
if metadata:
|
|
16
|
+
data['metadata'] = requests.utils.requote_uri(str(metadata))
|
|
17
|
+
if idempotency_key:
|
|
18
|
+
data['idempotency_key'] = idempotency_key
|
|
19
|
+
resp = requests.post(url, files=files, data=data)
|
|
20
|
+
resp.raise_for_status()
|
|
21
|
+
return resp.json()
|
|
22
|
+
|
|
23
|
+
def download(self, artifact_id: str, dest_path: str):
|
|
24
|
+
url = f"{self.base_url}/artifacts/{artifact_id}"
|
|
25
|
+
with requests.get(url, stream=True) as r:
|
|
26
|
+
r.raise_for_status()
|
|
27
|
+
os.makedirs(os.path.dirname(dest_path) or '.', exist_ok=True)
|
|
28
|
+
with open(dest_path, 'wb') as f:
|
|
29
|
+
for chunk in r.iter_content(chunk_size=CHUNK_SIZE):
|
|
30
|
+
if chunk:
|
|
31
|
+
f.write(chunk)
|
|
32
|
+
return dest_path
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pydorky-client"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pydorky Python client (minimal scaffold)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
|
|
8
|
+
[project.dependencies]
|
|
9
|
+
requests = "^2.28"
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
|
|
4
|
+
from dorky_client import DorkyClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_upload_and_download(tmp_path):
|
|
8
|
+
client = DorkyClient('http://localhost:3000')
|
|
9
|
+
|
|
10
|
+
src = tmp_path / 'hello.txt'
|
|
11
|
+
src.write_bytes(b'hello-pydorky')
|
|
12
|
+
|
|
13
|
+
resp = client.upload(str(src), metadata={'test': 'integration'})
|
|
14
|
+
assert 'id' in resp
|
|
15
|
+
artifact_id = resp['id']
|
|
16
|
+
|
|
17
|
+
dest = tmp_path / 'out.txt'
|
|
18
|
+
client.download(artifact_id, str(dest))
|
|
19
|
+
assert dest.exists()
|
|
20
|
+
assert dest.read_bytes() == b'hello-pydorky'
|
package/rectdorky.png
ADDED
|
Binary file
|
package/server/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const multer = require('multer');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const upload = multer({ dest: 'tmp/' });
|
|
8
|
+
const app = express();
|
|
9
|
+
const PORT = process.env.PORT || 3000;
|
|
10
|
+
|
|
11
|
+
// In-memory storage for metadata and idempotency (in production: use database)
|
|
12
|
+
const artifactStore = new Map(); // id -> { path, metadata, versions }
|
|
13
|
+
const idempotencyStore = new Map(); // idempotency_key -> artifact_id
|
|
14
|
+
|
|
15
|
+
// Utility: compute SHA256 hash of file
|
|
16
|
+
function hashFile(filePath) {
|
|
17
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
18
|
+
return crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Health check endpoint
|
|
22
|
+
app.get('/health', (req, res) => {
|
|
23
|
+
res.json({
|
|
24
|
+
status: 'healthy',
|
|
25
|
+
timestamp: new Date().toISOString()
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Serve OpenAPI spec
|
|
30
|
+
app.get('/openapi.yaml', (req, res) => {
|
|
31
|
+
res.sendFile(path.resolve(__dirname, '..', 'openapi', 'openapi.yaml'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// POST /artifacts - Upload artifact with metadata and idempotency
|
|
35
|
+
app.post('/artifacts', upload.single('file'), (req, res) => {
|
|
36
|
+
if (!req.file) {
|
|
37
|
+
return res.status(400).json({ error: 'no file provided' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const idempotencyKey = req.body.idempotency_key;
|
|
41
|
+
|
|
42
|
+
// Check idempotency: if same key uploaded before, return cached result
|
|
43
|
+
if (idempotencyKey && idempotencyStore.has(idempotencyKey)) {
|
|
44
|
+
const existingId = idempotencyStore.get(idempotencyKey);
|
|
45
|
+
const existing = artifactStore.get(existingId);
|
|
46
|
+
return res.status(409).json({
|
|
47
|
+
error: 'artifact already exists with this idempotency key',
|
|
48
|
+
artifact: {
|
|
49
|
+
id: existingId,
|
|
50
|
+
url: `/artifacts/${existingId}`,
|
|
51
|
+
metadata: existing.metadata
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate artifact ID
|
|
57
|
+
const id = Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 9);
|
|
58
|
+
const dest = path.resolve(__dirname, '..', 'data', id + '_' + req.file.originalname);
|
|
59
|
+
|
|
60
|
+
// Create data directory
|
|
61
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
62
|
+
fs.renameSync(req.file.path, dest);
|
|
63
|
+
|
|
64
|
+
// Compute file hash and size
|
|
65
|
+
const fileSize = fs.statSync(dest).size;
|
|
66
|
+
const contentHash = hashFile(dest);
|
|
67
|
+
|
|
68
|
+
// Parse custom metadata
|
|
69
|
+
let customMetadata = {};
|
|
70
|
+
try {
|
|
71
|
+
if (req.body.metadata) {
|
|
72
|
+
customMetadata = JSON.parse(req.body.metadata);
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return res.status(400).json({ error: 'invalid metadata JSON' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build complete metadata
|
|
79
|
+
const metadata = {
|
|
80
|
+
content_hash: contentHash,
|
|
81
|
+
file_size: fileSize,
|
|
82
|
+
compression_type: null,
|
|
83
|
+
compressed_size: fileSize,
|
|
84
|
+
last_updated: new Date().toISOString(),
|
|
85
|
+
uploaded_by: req.body.uploaded_by || 'anonymous',
|
|
86
|
+
custom: customMetadata
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Store artifact and metadata
|
|
90
|
+
artifactStore.set(id, {
|
|
91
|
+
path: dest,
|
|
92
|
+
metadata: metadata,
|
|
93
|
+
versions: [{ version_id: '1', created_at: metadata.last_updated, metadata: metadata }]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Store idempotency mapping
|
|
97
|
+
if (idempotencyKey) {
|
|
98
|
+
idempotencyStore.set(idempotencyKey, id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
res.status(201).json({
|
|
102
|
+
id,
|
|
103
|
+
url: `/artifacts/${id}`,
|
|
104
|
+
metadata,
|
|
105
|
+
version: '1'
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// GET /artifacts/{id} - Download artifact
|
|
110
|
+
app.get('/artifacts/:id', (req, res) => {
|
|
111
|
+
const id = req.params.id;
|
|
112
|
+
const artifact = artifactStore.get(id);
|
|
113
|
+
|
|
114
|
+
if (!artifact) {
|
|
115
|
+
// Fallback: check legacy data directory
|
|
116
|
+
const dir = path.resolve(__dirname, '..', 'data');
|
|
117
|
+
if (fs.existsSync(dir)) {
|
|
118
|
+
const file = fs.readdirSync(dir).find(f => f.startsWith(id + '_'));
|
|
119
|
+
if (file) {
|
|
120
|
+
return res.sendFile(path.resolve(dir, file));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return res.status(404).json({ error: 'artifact not found' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
res.sendFile(artifact.path);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// DELETE /artifacts/{id} - Delete artifact
|
|
130
|
+
app.delete('/artifacts/:id', (req, res) => {
|
|
131
|
+
const id = req.params.id;
|
|
132
|
+
const artifact = artifactStore.get(id);
|
|
133
|
+
|
|
134
|
+
if (!artifact) {
|
|
135
|
+
return res.status(404).json({ error: 'artifact not found' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Delete file from disk
|
|
139
|
+
if (fs.existsSync(artifact.path)) {
|
|
140
|
+
fs.unlinkSync(artifact.path);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove from store
|
|
144
|
+
artifactStore.delete(id);
|
|
145
|
+
|
|
146
|
+
res.status(204).send();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// GET /artifacts/{id}/metadata - Get metadata
|
|
150
|
+
app.get('/artifacts/:id/metadata', (req, res) => {
|
|
151
|
+
const id = req.params.id;
|
|
152
|
+
const artifact = artifactStore.get(id);
|
|
153
|
+
|
|
154
|
+
if (!artifact) {
|
|
155
|
+
return res.status(404).json({ error: 'artifact not found' });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
res.json(artifact.metadata);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// PATCH /artifacts/{id}/metadata - Update metadata
|
|
162
|
+
app.patch('/artifacts/:id/metadata', express.json(), (req, res) => {
|
|
163
|
+
const id = req.params.id;
|
|
164
|
+
const artifact = artifactStore.get(id);
|
|
165
|
+
|
|
166
|
+
if (!artifact) {
|
|
167
|
+
return res.status(404).json({ error: 'artifact not found' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Merge custom metadata
|
|
171
|
+
artifact.metadata.custom = { ...artifact.metadata.custom, ...req.body };
|
|
172
|
+
artifact.metadata.last_updated = new Date().toISOString();
|
|
173
|
+
|
|
174
|
+
res.json(artifact.metadata);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// GET /artifacts/{id}/versions - List all versions
|
|
178
|
+
app.get('/artifacts/:id/versions', (req, res) => {
|
|
179
|
+
const id = req.params.id;
|
|
180
|
+
const artifact = artifactStore.get(id);
|
|
181
|
+
|
|
182
|
+
if (!artifact) {
|
|
183
|
+
return res.status(404).json({ error: 'artifact not found' });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
res.json(artifact.versions || []);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
app.listen(PORT, () => {
|
|
190
|
+
console.log(`Dorky artifact service listening on port ${PORT}`);
|
|
191
|
+
console.log(`Health check: GET http://localhost:${PORT}/health`);
|
|
192
|
+
console.log(`OpenAPI spec: GET http://localhost:${PORT}/openapi.yaml`);
|
|
193
|
+
});
|