@iskra-bun/web-kit 0.1.0 → 0.2.0

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.
@@ -1,133 +0,0 @@
1
- import { BaseStorageAdapter, type PutOptions, type StorageConfig, type StorageFile } from "../base";
2
- import path from "node:path";
3
- import fs from "node:fs/promises";
4
- import { existsSync } from "node:fs";
5
-
6
- export class LocalStorageAdapter extends BaseStorageAdapter {
7
- private basePath: string;
8
-
9
- constructor(config: StorageConfig) {
10
- super();
11
- this.basePath = config.basePath || "./storage";
12
- }
13
-
14
- async connect(): Promise<void> {
15
- await fs.mkdir(this.basePath, { recursive: true });
16
- this.connected = true;
17
- console.log(`✅ Local storage connected at: ${this.basePath}`);
18
- }
19
-
20
- async disconnect(): Promise<void> {
21
- this.connected = false;
22
- }
23
-
24
- async put(filePath: string, data: Uint8Array | Buffer, options?: PutOptions): Promise<StorageFile> {
25
- this.ensureConnected();
26
-
27
- const sanitizedPath = this.sanitizePath(filePath);
28
- const fullPath = path.join(this.basePath, sanitizedPath);
29
-
30
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
31
- await fs.writeFile(fullPath, data);
32
-
33
- const stat = await fs.stat(fullPath);
34
-
35
- return {
36
- name: path.basename(sanitizedPath),
37
- path: sanitizedPath,
38
- size: stat.size,
39
- mimeType: options?.contentType || this.getMimeType(sanitizedPath),
40
- lastModified: stat.mtime,
41
- url: await this.url(sanitizedPath),
42
- };
43
- }
44
-
45
- async get(filePath: string): Promise<Uint8Array | null> {
46
- this.ensureConnected();
47
- const sanitizedPath = this.sanitizePath(filePath);
48
- const fullPath = path.join(this.basePath, sanitizedPath);
49
-
50
- try {
51
- return await fs.readFile(fullPath);
52
- } catch (error: any) {
53
- if (error.code === 'ENOENT') return null;
54
- throw error;
55
- }
56
- }
57
-
58
- async delete(filePath: string): Promise<void> {
59
- this.ensureConnected();
60
- const sanitizedPath = this.sanitizePath(filePath);
61
- const fullPath = path.join(this.basePath, sanitizedPath);
62
-
63
- try {
64
- await fs.unlink(fullPath);
65
- } catch (error: any) {
66
- if (error.code !== 'ENOENT') throw error;
67
- }
68
- }
69
-
70
- async exists(filePath: string): Promise<boolean> {
71
- this.ensureConnected();
72
- const sanitizedPath = this.sanitizePath(filePath);
73
- const fullPath = path.join(this.basePath, sanitizedPath);
74
- try {
75
- await fs.access(fullPath);
76
- return true;
77
- } catch {
78
- return false;
79
- }
80
- }
81
-
82
- async isDirectory(filePath: string): Promise<boolean> {
83
- this.ensureConnected();
84
- const sanitizedPath = this.sanitizePath(filePath);
85
- const fullPath = path.join(this.basePath, sanitizedPath);
86
- try {
87
- const stat = await fs.stat(fullPath);
88
- return stat.isDirectory();
89
- } catch {
90
- return false;
91
- }
92
- }
93
-
94
- async list(prefix?: string): Promise<StorageFile[]> {
95
- this.ensureConnected();
96
- const searchPath = prefix ? path.join(this.basePath, this.sanitizePath(prefix)) : this.basePath;
97
- const files: StorageFile[] = [];
98
-
99
- // Simple recursive walk
100
- const walk = async (dir: string) => {
101
- try {
102
- const entries = await fs.readdir(dir, { withFileTypes: true });
103
- for (const entry of entries) {
104
- const fullPath = path.join(dir, entry.name);
105
- if (entry.isDirectory()) {
106
- await walk(fullPath);
107
- } else {
108
- const relativePath = path.relative(this.basePath, fullPath);
109
- const stat = await fs.stat(fullPath);
110
- files.push({
111
- name: entry.name,
112
- path: this.sanitizePath(relativePath),
113
- size: stat.size,
114
- mimeType: this.getMimeType(entry.name),
115
- lastModified: stat.mtime,
116
- url: await this.url(this.sanitizePath(relativePath))
117
- });
118
- }
119
- }
120
- } catch (e: any) {
121
- if (e.code !== 'ENOENT') throw e;
122
- }
123
- };
124
-
125
- await walk(searchPath);
126
- return files;
127
- }
128
-
129
- async url(filePath: string, _expiresIn?: number): Promise<string> {
130
- const sanitizedPath = this.sanitizePath(filePath);
131
- return `/storage/${sanitizedPath}`;
132
- }
133
- }
@@ -1,193 +0,0 @@
1
- import { BaseStorageAdapter } from "../base";
2
- import type { StorageConfig, StorageFile, PutOptions } from "../base";
3
- import {
4
- S3Client,
5
- PutObjectCommand,
6
- GetObjectCommand,
7
- DeleteObjectCommand,
8
- HeadObjectCommand,
9
- HeadBucketCommand,
10
- ListObjectsV2Command,
11
- CopyObjectCommand,
12
- } from "@aws-sdk/client-s3";
13
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
14
-
15
- export class S3StorageAdapter extends BaseStorageAdapter {
16
- private client: S3Client;
17
- private bucket: string;
18
-
19
- constructor(private config: StorageConfig) {
20
- super();
21
- const conn = config.connection || {};
22
-
23
- this.bucket = conn.bucket || "iskra-storage";
24
-
25
- this.client = new S3Client({
26
- endpoint: conn.endpoint,
27
- region: conn.region || "us-east-1",
28
- credentials: conn.accessKey && conn.secretKey
29
- ? { accessKeyId: conn.accessKey, secretAccessKey: conn.secretKey }
30
- : undefined,
31
- forcePathStyle: !!conn.endpoint, // true for MinIO
32
- });
33
- }
34
-
35
- async connect(): Promise<void> {
36
- try {
37
- await this.client.send(new HeadBucketCommand({ Bucket: this.bucket }));
38
- this.connected = true;
39
- } catch (err: any) {
40
- throw new Error(`Failed to connect to S3 bucket "${this.bucket}": ${err.message}`);
41
- }
42
- }
43
-
44
- async disconnect(): Promise<void> {
45
- this.client.destroy();
46
- this.connected = false;
47
- }
48
-
49
- async put(path: string, data: Uint8Array | Buffer | ReadableStream, options?: PutOptions): Promise<StorageFile> {
50
- this.ensureConnected();
51
- const key = this.sanitizePath(path);
52
-
53
- let body: Uint8Array | Buffer;
54
- if (data instanceof ReadableStream) {
55
- const response = new Response(data);
56
- body = new Uint8Array(await response.arrayBuffer());
57
- } else {
58
- body = data;
59
- }
60
-
61
- await this.client.send(new PutObjectCommand({
62
- Bucket: this.bucket,
63
- Key: key,
64
- Body: body,
65
- ContentType: options?.contentType || this.getMimeType(key),
66
- Metadata: options?.metadata,
67
- }));
68
-
69
- return {
70
- name: key.split("/").pop() || key,
71
- path: key,
72
- size: body.length,
73
- mimeType: options?.contentType || this.getMimeType(key),
74
- lastModified: new Date(),
75
- };
76
- }
77
-
78
- async get(path: string): Promise<Uint8Array | null> {
79
- this.ensureConnected();
80
- const key = this.sanitizePath(path);
81
-
82
- try {
83
- const response = await this.client.send(new GetObjectCommand({
84
- Bucket: this.bucket,
85
- Key: key,
86
- }));
87
-
88
- if (!response.Body) return null;
89
- return new Uint8Array(await response.Body.transformToByteArray());
90
- } catch (err: any) {
91
- if (err.name === "NoSuchKey" || err.$metadata?.httpStatusCode === 404) {
92
- return null;
93
- }
94
- throw err;
95
- }
96
- }
97
-
98
- async delete(path: string): Promise<void> {
99
- this.ensureConnected();
100
- const key = this.sanitizePath(path);
101
-
102
- await this.client.send(new DeleteObjectCommand({
103
- Bucket: this.bucket,
104
- Key: key,
105
- }));
106
- }
107
-
108
- async exists(path: string): Promise<boolean> {
109
- this.ensureConnected();
110
- const key = this.sanitizePath(path);
111
-
112
- try {
113
- await this.client.send(new HeadObjectCommand({
114
- Bucket: this.bucket,
115
- Key: key,
116
- }));
117
- return true;
118
- } catch (err: any) {
119
- if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
120
- return false;
121
- }
122
- throw err;
123
- }
124
- }
125
-
126
- async list(prefix?: string): Promise<StorageFile[]> {
127
- this.ensureConnected();
128
- const files: StorageFile[] = [];
129
- let continuationToken: string | undefined;
130
-
131
- do {
132
- const response = await this.client.send(new ListObjectsV2Command({
133
- Bucket: this.bucket,
134
- Prefix: prefix ? this.sanitizePath(prefix) : undefined,
135
- ContinuationToken: continuationToken,
136
- }));
137
-
138
- if (response.Contents) {
139
- for (const obj of response.Contents) {
140
- if (!obj.Key) continue;
141
- files.push({
142
- name: obj.Key.split("/").pop() || obj.Key,
143
- path: obj.Key,
144
- size: obj.Size || 0,
145
- mimeType: this.getMimeType(obj.Key),
146
- lastModified: obj.LastModified,
147
- });
148
- }
149
- }
150
-
151
- continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined;
152
- } while (continuationToken);
153
-
154
- return files;
155
- }
156
-
157
- async url(path: string, expiresIn: number = 3600): Promise<string> {
158
- this.ensureConnected();
159
- const key = this.sanitizePath(path);
160
-
161
- const command = new GetObjectCommand({
162
- Bucket: this.bucket,
163
- Key: key,
164
- });
165
-
166
- return await getSignedUrl(this.client, command, { expiresIn });
167
- }
168
-
169
- async copy(from: string, to: string): Promise<void> {
170
- this.ensureConnected();
171
- const sourceKey = this.sanitizePath(from);
172
- const destKey = this.sanitizePath(to);
173
-
174
- await this.client.send(new CopyObjectCommand({
175
- Bucket: this.bucket,
176
- CopySource: `${this.bucket}/${sourceKey}`,
177
- Key: destKey,
178
- }));
179
- }
180
-
181
- async isDirectory(path: string): Promise<boolean> {
182
- this.ensureConnected();
183
- const prefix = this.sanitizePath(path).replace(/\/?$/, "/");
184
-
185
- const response = await this.client.send(new ListObjectsV2Command({
186
- Bucket: this.bucket,
187
- Prefix: prefix,
188
- MaxKeys: 1,
189
- }));
190
-
191
- return (response.Contents?.length || 0) > 0;
192
- }
193
- }
@@ -1,112 +0,0 @@
1
- import fs from "node:fs/promises";
2
-
3
- export interface StorageConfig {
4
- adapter: "local" | "minio" | "s3";
5
- basePath?: string; // For local storage
6
- connection?: {
7
- endpoint?: string;
8
- accessKey?: string;
9
- secretKey?: string;
10
- bucket?: string;
11
- region?: string;
12
- useSSL?: boolean;
13
- };
14
- }
15
-
16
- export interface StorageFile {
17
- name: string;
18
- path: string;
19
- size: number;
20
- mimeType?: string;
21
- lastModified?: Date;
22
- url?: string;
23
- }
24
-
25
- export interface PutOptions {
26
- contentType?: string;
27
- metadata?: Record<string, string>;
28
- public?: boolean;
29
- }
30
-
31
- export interface StorageAdapter {
32
- connect(): Promise<void>;
33
- disconnect(): Promise<void>;
34
- put(path: string, data: Uint8Array | Buffer | ReadableStream, options?: PutOptions): Promise<StorageFile>;
35
- get(path: string): Promise<Uint8Array | null>;
36
- // getStream(path: string): Promise<ReadableStream | null>;
37
- // Node streams are different, sticking to Buffer/Uint8Array for simplicity or using generic Stream
38
- delete(path: string): Promise<void>;
39
- exists(path: string): Promise<boolean>;
40
- list(prefix?: string): Promise<StorageFile[]>;
41
- url(path: string, expiresIn?: number): Promise<string>;
42
- copy(from: string, to: string): Promise<void>;
43
- move(from: string, to: string): Promise<void>;
44
- isDirectory(path: string): Promise<boolean>;
45
- }
46
-
47
- export abstract class BaseStorageAdapter implements StorageAdapter {
48
- protected connected = false;
49
-
50
- abstract connect(): Promise<void>;
51
- abstract disconnect(): Promise<void>;
52
- abstract put(path: string, data: Uint8Array | Buffer | ReadableStream, options?: PutOptions): Promise<StorageFile>;
53
- abstract get(path: string): Promise<Uint8Array | null>;
54
-
55
- abstract delete(path: string): Promise<void>;
56
- abstract exists(path: string): Promise<boolean>;
57
- abstract list(prefix?: string): Promise<StorageFile[]>;
58
- abstract url(path: string, expiresIn?: number): Promise<string>;
59
- abstract isDirectory(path: string): Promise<boolean>;
60
-
61
- isConnected(): boolean {
62
- return this.connected;
63
- }
64
-
65
- protected ensureConnected(): void {
66
- if (!this.connected) {
67
- throw new Error("Storage not connected. Call connect() first.");
68
- }
69
- }
70
-
71
- async copy(from: string, to: string): Promise<void> {
72
- const data = await this.get(from);
73
- if (!data) {
74
- throw new Error(`Source file not found: ${from}`);
75
- }
76
- await this.put(to, data);
77
- }
78
-
79
- async move(from: string, to: string): Promise<void> {
80
- await this.copy(from, to);
81
- await this.delete(from);
82
- }
83
-
84
- protected generateFileName(originalName: string): string {
85
- const ext = originalName.split(".").pop();
86
- const timestamp = Date.now();
87
- const random = Math.random().toString(36).substring(2, 15);
88
- return `${timestamp}-${random}.${ext}`;
89
- }
90
-
91
- protected sanitizePath(path: string): string {
92
- return path
93
- .replace(/^\/+/, "")
94
- .replace(/\/+/g, "/")
95
- .replace(/\\/g, "/");
96
- }
97
-
98
- protected getMimeType(filename: string): string {
99
- const ext = filename.split(".").pop()?.toLowerCase();
100
- const mimeTypes: Record<string, string> = {
101
- jpg: "image/jpeg",
102
- jpeg: "image/jpeg",
103
- png: "image/png",
104
- gif: "image/gif",
105
- pdf: "application/pdf",
106
- txt: "text/plain",
107
- json: "application/json",
108
- zip: "application/zip",
109
- };
110
- return mimeTypes[ext || ""] || "application/octet-stream";
111
- }
112
- }