-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Init storage shipper #2
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
module.exports = { | ||
"env": { | ||
"commonjs": true, | ||
"es2021": true, | ||
"node": true | ||
}, | ||
"extends": [ | ||
"plugin:jest/all", | ||
"standard-with-typescript" | ||
], | ||
"parserOptions": { | ||
"ecmaVersion": "latest", | ||
"sourceType": "module", | ||
"project": [ | ||
"./tsconfig.json" | ||
] | ||
}, | ||
"plugins": [ | ||
"jest" | ||
], | ||
"rules": { | ||
"jest/prefer-expect-assertions": 0 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.idea/ | ||
dist/ | ||
node_modules/ | ||
lerna-debug.log | ||
package-lock.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"$schema": "node_modules/lerna/schemas/lerna-schema.json", | ||
"version": "0.0.0" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"name": "root", | ||
"private": false, | ||
"workspaces": [ | ||
"packages/*" | ||
], | ||
"devDependencies": { | ||
"@azure/storage-blob": "12.15.0", | ||
"@types/jest": "29.5.3", | ||
"@typescript-eslint/eslint-plugin": "5.52.0", | ||
"eslint": "8.46.0", | ||
"eslint-config-standard-with-typescript": "37.0.0", | ||
"eslint-plugin-import": "2.28.0", | ||
"eslint-plugin-jest": "27.2.3", | ||
"eslint-plugin-n": "16.0.1", | ||
"eslint-plugin-promise": "6.1.1", | ||
"jest": "29.6.2", | ||
"lerna": "7.1.4", | ||
"ts-jest": "29.1.1", | ||
"ts-node": "10.9.1", | ||
"typescript": "5.1.6" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# `core` | ||
|
||
> TODO: description | ||
|
||
## Usage | ||
|
||
``` | ||
const core = require('core'); | ||
|
||
// TODO: DEMONSTRATE API | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { ManifestValidator } from '../../src/manifest/ManifestValidator' | ||
|
||
describe('manifest validator test', () => { | ||
it('should not throw exception when json is valid', () => { | ||
// given | ||
const manifest = { | ||
version: '1.0', | ||
deploy: [ | ||
{ | ||
name: 'resources', | ||
include: 'v1-rc6/resources/*', | ||
headers: { | ||
'content-cache': 'public, max-age=2592000' | ||
} | ||
}, | ||
{ | ||
name: 'main', | ||
include: 'v1-rc6/*.js', | ||
headers: { | ||
'content-encoding': 'gzip', | ||
'content-type': 'text/javascript', | ||
'content-cache': 'public, max-age=2592000' | ||
} | ||
}, | ||
{ | ||
name: 'chat-loader', | ||
include: 'chat-loader.js', | ||
headers: { | ||
'content-encoding': 'gzip', | ||
'content-typ': 'text/javascript', | ||
'content-cache': 'public, max-age=300' | ||
} | ||
} | ||
] | ||
} | ||
|
||
const manifestValidator = new ManifestValidator() | ||
|
||
// when/then | ||
expect(() => { | ||
manifestValidator.validate(manifest) | ||
}).not.toThrow() | ||
}) | ||
|
||
it('should throw exception when json is not valid', () => { | ||
// given | ||
const manifest = { | ||
version: 'invalid-version', | ||
deploy: [ | ||
{ | ||
name: 'resources', | ||
include: 'v1-rc6/resources/*', | ||
headers: { | ||
'content-cache': 'public, max-age=2592000' | ||
} | ||
}, | ||
{ | ||
name: 'main', | ||
include: 'v1-rc6/*.js', | ||
headers: { | ||
'content-encoding': 'gzip', | ||
'content-type': 'text/javascript', | ||
'content-cache': 'public, max-age=2592000' | ||
} | ||
}, | ||
{ | ||
name: 'chat-loader', | ||
include: 'chat-loader.js', | ||
headers: { | ||
'content-encoding': 'gzip', | ||
'content-typ': 'text/javascript', | ||
'content-cache': 'public, max-age=300' | ||
} | ||
} | ||
] | ||
} | ||
|
||
const manifestValidator = new ManifestValidator() | ||
|
||
// when/then | ||
expect(() => { | ||
manifestValidator.validate(manifest) | ||
}).toThrow('Storage shipper manifest is not valid. Error: [path: \'/version\'] [message: \'must match pattern "^([1-9]\\d*)\\.(?:0|[1-9]\\d*)$"\']') | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { type JestConfigWithTsJest } from 'ts-jest' | ||
|
||
const config: JestConfigWithTsJest = { | ||
preset: 'ts-jest' | ||
} | ||
|
||
export default config |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "@thulium/storage-shipper", | ||
"version": "0.0.0", | ||
"description": "Core module for ship files to storage", | ||
"homepage": "https://github.com/thulium/storage-shipper#readme", | ||
"license": "MIT", | ||
"main": "src/core.ts", | ||
"directories": { | ||
"lib": "src", | ||
"test": "__tests__" | ||
}, | ||
"files": [ | ||
"src" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/thulium/storage-shipper.git" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"test": "jest", | ||
"eslint": "eslint \"src/**\"" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/thulium/storage-shipper/issues" | ||
}, | ||
"dependencies": { | ||
"ajv": "8.12.0", | ||
"jszip": "3.10.1", | ||
"minimatch": "9.0.3", | ||
"remeda": "1.24.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface Destination { | ||
container: string | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { type StorageUploader } from './StorageUploader' | ||
import { type Destination } from './Destination' | ||
import { type ArtifactRepositoryFactory } from './artifact/ArtifactRepositoryFactory' | ||
import { type Artifact } from './artifact/Artifact' | ||
|
||
export class StorageShipper { | ||
constructor ( | ||
private readonly artifactRepositoryFactory: ArtifactRepositoryFactory, | ||
private readonly storageUploader: StorageUploader | ||
) { | ||
} | ||
|
||
public async shipIt (artifact: Artifact, destination: Destination): Promise<void> { | ||
const artifactRepository = await this.artifactRepositoryFactory.create(artifact) | ||
|
||
const manifest = await artifactRepository.getManifest() | ||
|
||
const deploys = manifest.deploy | ||
for (const deploy of deploys) { | ||
const artifactFiles = await artifactRepository.getMatchingArtifactFiles(deploy.include) | ||
this.storageUploader.upload(artifactFiles, destination, artifact) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { type Destination } from './Destination' | ||
import { type Artifact, type ArtifactFile } from './artifact/Artifact' | ||
|
||
export interface StorageUploader { | ||
upload: (artifactFiles: ArtifactFile[], destination: Destination, artifact: Artifact) => void | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export interface Artifact { | ||
parentDir?: string | ||
path: string | ||
} | ||
|
||
export interface ArtifactFile { | ||
name: string | ||
data: ArrayBuffer | null | ||
} | ||
|
||
export function sanitizedParentDir (artifact: Artifact): string | null | undefined { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tą funkcje proponuje przenieść do src/utils/artifact.ts |
||
const parentDir = artifact.parentDir | ||
if (parentDir == null) { | ||
return parentDir | ||
} | ||
return parentDir.endsWith('/') ? '' : '/' | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { type Manifest } from '../manifest/Manifest' | ||
import { type ArtifactFile } from './Artifact' | ||
|
||
export interface ArtifactRepository { | ||
getManifest: () => Promise<Manifest> | ||
|
||
getMatchingArtifactFiles: (pattern: string) => Promise<ArtifactFile[]> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { type Artifact } from './Artifact' | ||
import { type ArtifactRepository } from './ArtifactRepository' | ||
|
||
export interface ArtifactRepositoryFactory { | ||
create: (artifact: Artifact) => Promise<ArtifactRepository> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import type JSZip from 'jszip' | ||
import { minimatch } from 'minimatch/dist/mjs' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. racze sporadycznie jest potrzeba importowania z dist/, jeżeli zrobiłeś tak bo ts rzucał błędy to raczej kwestia braku typów, wtedy jesli biblioteka nie jest w TS to warto poszukać typów do niej -> https://www.npmjs.com/package/@types/minimatch |
||
import { type Artifact, type ArtifactFile, sanitizedParentDir } from '../Artifact' | ||
import { ManifestValidator } from '../../manifest/ManifestValidator' | ||
import { type Manifest } from '../../manifest/Manifest' | ||
import { type ArtifactRepository } from '../ArtifactRepository' | ||
import * as R from 'remeda' | ||
|
||
export class ZipArtifactRepository implements ArtifactRepository { | ||
private readonly manifestValidator: ManifestValidator = new ManifestValidator() | ||
|
||
constructor ( | ||
private readonly artifact: Artifact, | ||
private readonly jsZip: JSZip | ||
) { | ||
} | ||
|
||
public async getManifest (): Promise<Manifest> { | ||
const manifests = this.jsZip.filter(relativePath => { | ||
return relativePath.includes('storage-shipper-manifest.json') | ||
}) | ||
|
||
const manifestString = await this.getManifestString(manifests) | ||
const manifest: Manifest = JSON.parse(manifestString) | ||
this.manifestValidator.validate(manifest) | ||
|
||
return manifest | ||
} | ||
|
||
public async getMatchingArtifactFiles (pattern: string): Promise<ArtifactFile[]> { | ||
const jsZipObjects = this.jsZip.filter(relativePath => { | ||
const parentDir = sanitizedParentDir(this.artifact) | ||
if (parentDir != null) { | ||
relativePath = relativePath.replace(parentDir, '') | ||
} | ||
return minimatch(relativePath, pattern) | ||
}) | ||
|
||
const artifactFilesPromise = R.map(jsZipObjects, async (jsZipObject): Promise<ArtifactFile> => { | ||
const parentDir = sanitizedParentDir(this.artifact) ?? '' | ||
const data = await jsZipObject.async('arraybuffer') ?? null | ||
return { | ||
name: jsZipObject.name.replace(parentDir, ''), | ||
data | ||
} | ||
}) | ||
return await Promise.all(artifactFilesPromise) | ||
} | ||
|
||
private async getManifestString (manifests: JSZip.JSZipObject[]): Promise<string> { | ||
if (manifests.length !== 1) { | ||
throw new Error('Cannot determine correct manifest file') | ||
} | ||
|
||
const manifestFile = manifests.at(0) | ||
const manifestString = await manifestFile?.async('text') ?? null | ||
if (manifestString === null) { | ||
throw new Error('Cannot read manifest file') | ||
} | ||
|
||
return manifestString | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import JSZip from 'jszip' | ||
import { type ArtifactRepository } from '../ArtifactRepository' | ||
import * as fs from 'fs' | ||
import { type ArtifactRepositoryFactory } from '../ArtifactRepositoryFactory' | ||
import { type Artifact } from '../Artifact' | ||
import { ZipArtifactRepository } from './ZipArtifactRepository' | ||
|
||
export class ZipArtifactRepositoryFactory implements ArtifactRepositoryFactory { | ||
private readonly jsZip = new JSZip() | ||
|
||
public async create (artifact: Artifact): Promise<ArtifactRepository> { | ||
const path = artifact.path | ||
|
||
if (!fs.existsSync(path)) { | ||
throw new Error(`Artifact '${path}' does not exists`) | ||
} | ||
|
||
const archive = fs.readFileSync(path) | ||
const jsZip = await this.jsZip.loadAsync(archive) | ||
return new ZipArtifactRepository(artifact, jsZip) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
export * from './Destination' | ||
export * from './StorageShipper' | ||
export * from './StorageUploader' | ||
|
||
export * from './artifact/Artifact' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. w tym pliku powinny być eksporty tylko tych funkcji, które użytkownik bibloteki może używać, bez eksportu typów (typy powinny sie przy buildzie wygenerować automatycznie jako pliki xyz.d.ts |
||
export * from './artifact/ArtifactRepositoryFactory' | ||
|
||
export * from './artifact/zip/ZipArtifactRepository' | ||
export * from './artifact/zip/ZipArtifactRepositoryFactory' | ||
|
||
export * from './manifest/Manifest' | ||
export * from './manifest/ManifestValidator' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export interface Manifest { | ||
version: string | ||
deploy: Deploy[] | ||
} | ||
|
||
export interface Deploy { | ||
name: string | ||
include: string | ||
headers?: object | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. jak chcesz typ dla dowolnego objektu to lepiej użyć Record<string, unknown> (object to może być wszystko prawie, nawet null) |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jeśli chodzi o typy to zazwyczaj robimy tak że: