Skip to content

Commit

Permalink
fix: fs minio
Browse files Browse the repository at this point in the history
  • Loading branch information
izatop committed Apr 6, 2023
1 parent d6fa092 commit f9e029d
Show file tree
Hide file tree
Showing 12 changed files with 1,219 additions and 472 deletions.
4 changes: 3 additions & 1 deletion packages/fs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"@bunt/unit": "^0.25.0",
"@bunt/util": "^0.25.0",
"@types/minio": "^7.0.15",
"minio": "^7.0.32"
"@types/node-fetch": "^2.6.3",
"minio": "^7.0.32",
"node-fetch": "2"
},
"license": "MIT"
}
15 changes: 11 additions & 4 deletions packages/fs/src/Bucket/FsBucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as https from "node:https";
import {Readable} from "node:stream";
import {FileStorage} from "../FileStorage";
import {FsDriverAbstract} from "../Driver/FsDriverAbstract";
import {FsWritableFile, IBucketOptions} from "../interfaces";
import {FsSource, FsStat, FsWritable, IBucketOptions} from "../interfaces";

export class FsBucket {
public readonly name: string;
Expand All @@ -24,6 +24,14 @@ export class FsBucket {
return this.#driver.getBucketPolicy(this.name);
}

public get(name: string): Promise<Readable> {
return this.#driver.get(this.name, name);
}

public put(name: string, source: FsSource, md?: Record<string, any>): Promise<FsStat> {
return this.#driver.put(this.name, name, source, md);
}

public getPresignedUrl(file: string, expire: number = 7 * 24 * 60 * 60): Promise<string> {
return this.#driver.getPresignedUrl(this.name, file, expire);
}
Expand All @@ -40,16 +48,15 @@ export class FsBucket {
return this.#driver.deletePresignedUrl(this.name, file, expire);
}

public async write(path: string, file: FsWritableFile, metadata: Record<any, any>): Promise<string> {
public async write(path: string, file: FsWritable, metadata: Record<any, any>): Promise<string> {
return this.#driver.write(this.name, path, file, metadata);
}

public async writeRemoteURL(path: string, url: string, metadata: Record<any, any>): Promise<string> {
const get = url.startsWith("https") ? https.get : http.get;
const stream = await new Promise<Readable>((resolve, reject) => (
const stream = await new Promise<http.IncomingMessage>((resolve, reject) => (
get(url, (res) => resolve(res))
.on("error", reject)
.end()
));

return this.write(path, stream, metadata);
Expand Down
24 changes: 20 additions & 4 deletions packages/fs/src/Driver/FsDriverAbstract.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import {FsWritableFile} from "../interfaces";
import {Readable} from "node:stream";
import {FsSource, FsStat, FsWritable} from "../interfaces";

export abstract class FsDriverAbstract {
public abstract createBucket(name: string, region?: string, checkExists?: boolean): Promise<void>;

public abstract write(bucket: string, name: string, file: FsWritableFile, metadata: Record<any, any>)
: Promise<string>;

public abstract setBucketPolicy(bucket: string, policy: string): Promise<void>;

public abstract getBucketPolicy(bucket: string): Promise<string>;
Expand All @@ -17,4 +15,22 @@ export abstract class FsDriverAbstract {
public abstract removeObject(bucket: string, file: string): Promise<void>;

public abstract deletePresignedUrl(bucket: string, file: string, expire: number): Promise<string>;

public abstract get(bucket: string, file: string): Promise<Readable>;

public abstract stat(bucket: string, file: string): Promise<FsStat>;

public abstract write(
bucket: string,
name: string,
file: FsWritable,
metadata: Record<any, any>
): Promise<string>;

public abstract put(
bucket: string,
name: string,
file: FsSource,
metadata?: Record<string, any>
): Promise<FsStat>;
}
72 changes: 67 additions & 5 deletions packages/fs/src/Driver/MinIO.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {URL} from "url";
import {Readable} from "stream";
import {assert, isString} from "@bunt/util";
import {Client} from "minio";
import {FsWritableFile} from "../interfaces";
import {Client, UploadedObjectInfo} from "minio";
import fetch from "node-fetch";
import {FsSource, FsStat, FsWritable} from "../interfaces";
import {FsDriverAbstract} from "./FsDriverAbstract";
import {MinIOBucketPolicy} from "./MinIOBucketPolicy";

const DEFAULT_REGION = "default";
const protocols = ["http:", "https:"];

export class MinIO extends FsDriverAbstract {
readonly #client: Client;
Expand Down Expand Up @@ -58,12 +61,71 @@ export class MinIO extends FsDriverAbstract {
await this.#client.makeBucket(name, region ?? DEFAULT_REGION);
}

public async write(bucket: string, name: string, file: FsWritableFile, metadata: Record<any, any>)
public async write(bucket: string, name: string, file: FsWritable, metadata: Record<string, any>)
: Promise<string> {
const result = isString(file)
const op = isString(file)
? this.#client.fPutObject(bucket, name, file, metadata)
: this.#client.putObject(bucket, name, file, metadata);

return (await result).etag;
return (await op).etag;
}

public get(bucket: string, file: string): Promise<Readable> {
return this.#client.getObject(bucket, file);
}

public async stat(bucket: string, file: string): Promise<FsStat> {
const {metaData: metadata, ...stat} = await this.#client.statObject(bucket, file);

return {
metadata,
...stat,
};
}

public async put(
bucket: string,
name: string,
source: FsSource,
metadata?: Record<string, any>,
): Promise<FsStat> {
await this.#put(bucket, name, source, metadata);

return this.stat(bucket, name);
}

async #put(
bucket: string,
name: string,
source: FsSource,
metadata: Record<string, any> = {},
): Promise<UploadedObjectInfo> {
if (source instanceof URL) {
if (source.protocol.startsWith("file")) {
return this.#client.fPutObject(bucket, name, source.pathname, metadata);
}

if (protocols.includes(source.protocol)) {
const response = await fetch(source);
const known = ["content-type"];
const headers = Object.fromEntries(
[...response.headers.entries()]
.filter(([key]) => known.includes(key)),
);

assert(response.body, `Response body is null for URL ${source.href}`);

return this.#client.putObject(
bucket,
name,
response.body as Readable,
{...headers, ...metadata},
);
}

throw new Error(`Unsupported protocol: ${source.protocol}`);
}

return this.#client.putObject(bucket, name, source, metadata);
}
}
11 changes: 10 additions & 1 deletion packages/fs/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ export interface IBucketOptions {
region?: string;
}

export type FsWritableFile = string | Readable | Buffer;
export type FsWritable = string | Buffer | Readable;

export type FsSource = string | Buffer | Readable | URL;

export type FsStat = {
size: number;
etag: string;
lastModified: Date;
metadata: Record<string, any>;
};
5 changes: 5 additions & 0 deletions packages/fs/test/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
rootDir: "src",
};
60 changes: 60 additions & 0 deletions packages/fs/test/src/Main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {dirname, resolve} from "node:path";
import {createReadStream} from "node:fs";
import {FileStorage, MinIO, MinIOBucketPolicy} from "../../src";

const fs = new FileStorage(new MinIO("http://minioadmin:minioadmin@localhost:9000"));

beforeAll(async () => {
const b = fs.getBucket("test");
await b.save();
await b.setPolicy(MinIOBucketPolicy.PUBLIC_READONLY);
});

describe("MinIO", () => {
test("put URL", async () => {
const bucket = fs.getBucket("test");
const stat = await bucket.put("url", new URL("https://www.google.com/favicon.ico"));
expect(stat.size).toBeGreaterThan(0);
expect(stat.lastModified).toBeInstanceOf(Date);
expect(stat.metadata["content-type"]).toBe("image/x-icon");
expect(stat.etag.length).toBeGreaterThan(0);
});

test("put string", async () => {
const bucket = fs.getBucket("test");
const stat = await bucket.put("string", "hello", {"content-type": "text/plain"});
expect(stat.size).toBe(5);
expect(stat.lastModified).toBeInstanceOf(Date);
expect(stat.metadata["content-type"]).toBe("text/plain");
expect(stat.etag.length).toBeGreaterThan(0);
});

test("put buffer", async () => {
const bucket = fs.getBucket("test");
const stat = await bucket.put("buffer", Buffer.from("hello"), {"content-type": "text/plain"});
expect(stat.size).toBe(5);
expect(stat.lastModified).toBeInstanceOf(Date);
expect(stat.metadata["content-type"]).toBe("text/plain");
expect(stat.etag.length).toBeGreaterThan(0);
});

test("put file", async () => {
const bucket = fs.getBucket("test");
const file = new URL(`file://${resolve(dirname(__filename), "./hello.txt")}`);
const stat = await bucket.put("file", file, {"content-type": "text/plain"});
expect(stat.size).toBe(6);
expect(stat.lastModified).toBeInstanceOf(Date);
expect(stat.metadata["content-type"]).toBe("text/plain");
expect(stat.etag.length).toBeGreaterThan(0);
});

test("put readable", async () => {
const bucket = fs.getBucket("test");
const file = createReadStream(resolve(dirname(__filename), "./hello.txt"));
const stat = await bucket.put("readable", file, {"content-type": "text/plain"});
expect(stat.size).toBe(6);
expect(stat.lastModified).toBeInstanceOf(Date);
expect(stat.metadata["content-type"]).toBe("text/plain");
expect(stat.etag.length).toBeGreaterThan(0);
});
});
1 change: 1 addition & 0 deletions packages/fs/test/src/hello.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello
7 changes: 7 additions & 0 deletions packages/fs/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.test.json",
"include": [
"src",
"../src"
]
}
5 changes: 4 additions & 1 deletion run-tests
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
test "$(docker ps --filter name=redis-test -q)" = "" && \
(docker run --rm --name redis-test -p 6379:6379 -d redis:alpine || exit 1)

test "$(docker ps --filter name=minio-test -q)" = "" && \
(docker run --rm --name minio-test -p 9000:9000 -d minio/minio server /data || exit 1)

yarn jest $@

docker stop redis-test
docker stop redis-test minio-test
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"module": "CommonJS",
"target": "ES2022",
"lib": [
"ES2022"
Expand Down
Loading

0 comments on commit f9e029d

Please sign in to comment.