mikes-php-image-handler 1.0.0 → 1.1.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.
- package/README.md +17 -4
- package/dist/index.d.ts +21 -5
- package/dist/index.js +86 -10
- package/package.json +1 -1
- package/src/index.ts +104 -15
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ const fs = require('fs');
|
|
|
17
17
|
// Initialize the client
|
|
18
18
|
const imageClient = new VampireImageClient({
|
|
19
19
|
baseUrl: 'https://img.miketsak.gr',
|
|
20
|
-
apiKey: 'your_api_key_here'
|
|
20
|
+
apiKey: 'your_api_key_here',
|
|
21
|
+
timeoutMs: 30000, // Optional: Request timeout in ms (default: 30000)
|
|
22
|
+
maxRetries: 3 // Optional: Number of retries on network failure (default: 3)
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
async function run() {
|
|
@@ -33,7 +35,11 @@ async function run() {
|
|
|
33
35
|
console.error('Upload failed:', uploadResult.error);
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
// 2.
|
|
38
|
+
// 2. Upload a Base64 String
|
|
39
|
+
const base64Str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...";
|
|
40
|
+
const base64Result = await imageClient.uploadBase64(base64Str, 'avatar.png');
|
|
41
|
+
|
|
42
|
+
// 3. Delete an Image
|
|
37
43
|
if (uploadResult.success) {
|
|
38
44
|
const deleteResult = await imageClient.deleteImage(uploadResult.filename);
|
|
39
45
|
if (deleteResult.success) {
|
|
@@ -50,10 +56,17 @@ run();
|
|
|
50
56
|
### `new VampireImageClient(options)`
|
|
51
57
|
- `options.baseUrl`: The URL of your PHP image API (e.g., `https://img.miketsak.gr`).
|
|
52
58
|
- `options.apiKey`: The secret API key used for authentication.
|
|
59
|
+
- `options.timeoutMs`: (Optional) The timeout in milliseconds before a request aborts. Default: `30000`.
|
|
60
|
+
- `options.maxRetries`: (Optional) The number of automatic retries (with exponential backoff) for transient network failures. Default: `3`.
|
|
53
61
|
|
|
54
62
|
### `uploadImage(file, filename?)`
|
|
55
|
-
- `file`: Can be a `Buffer`, a `Blob`, or a `File` object.
|
|
56
|
-
- `filename`: Required if `file` is a Buffer. Represents the original name of the file (e.g., `avatar.png`).
|
|
63
|
+
- `file`: Can be a `Buffer`, a `Uint8Array`, a `Blob`, or a `File` object.
|
|
64
|
+
- `filename`: Required if `file` is a Buffer or Uint8Array. Represents the original name of the file (e.g., `avatar.png`).
|
|
65
|
+
- **Returns**: A Promise resolving to an object `{ success: boolean, url?: string, filename?: string, error?: string }`.
|
|
66
|
+
|
|
67
|
+
### `uploadBase64(base64String, filename)`
|
|
68
|
+
- `base64String`: The Base64 string of the image. It handles stripping the `data:image/...;base64,` prefix automatically if present.
|
|
69
|
+
- `filename`: The name to save the file as.
|
|
57
70
|
- **Returns**: A Promise resolving to an object `{ success: boolean, url?: string, filename?: string, error?: string }`.
|
|
58
71
|
|
|
59
72
|
### `deleteImage(filename)`
|
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
export interface VampireImageClientOptions {
|
|
2
2
|
apiKey: string;
|
|
3
3
|
baseUrl: string;
|
|
4
|
+
/** Default timeout in milliseconds for requests (default: 30000) */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Maximum number of retries for failed network requests (default: 3) */
|
|
7
|
+
maxRetries?: number;
|
|
4
8
|
}
|
|
5
9
|
export interface UploadResponse {
|
|
6
|
-
success
|
|
10
|
+
success: boolean;
|
|
7
11
|
url?: string;
|
|
8
12
|
filename?: string;
|
|
9
13
|
error?: string;
|
|
10
14
|
}
|
|
11
15
|
export interface DeleteResponse {
|
|
12
|
-
success
|
|
16
|
+
success: boolean;
|
|
13
17
|
message?: string;
|
|
14
18
|
error?: string;
|
|
15
19
|
}
|
|
16
20
|
export declare class VampireImageClient {
|
|
17
21
|
private apiKey;
|
|
18
22
|
private baseUrl;
|
|
23
|
+
private timeoutMs;
|
|
24
|
+
private maxRetries;
|
|
19
25
|
constructor(options: VampireImageClientOptions);
|
|
20
26
|
private getHeaders;
|
|
27
|
+
/**
|
|
28
|
+
* Internal fetch wrapper with timeout and retry logic
|
|
29
|
+
*/
|
|
30
|
+
private fetchWithRetry;
|
|
21
31
|
/**
|
|
22
32
|
* Upload an image to the PHP server.
|
|
23
|
-
* @param file A standard File
|
|
24
|
-
* @param filename Optional filename (
|
|
33
|
+
* @param file A standard File, Blob, Buffer, or Uint8Array.
|
|
34
|
+
* @param filename Optional filename (required if passing a Buffer/Uint8Array)
|
|
35
|
+
*/
|
|
36
|
+
uploadImage(file: Blob | Uint8Array, filename?: string): Promise<UploadResponse>;
|
|
37
|
+
/**
|
|
38
|
+
* Upload a Base64 encoded image string to the PHP server.
|
|
39
|
+
* @param base64String The base64 string (can include the data URI prefix like "data:image/png;base64,...")
|
|
40
|
+
* @param filename The filename to save it as
|
|
25
41
|
*/
|
|
26
|
-
|
|
42
|
+
uploadBase64(base64String: string, filename: string): Promise<UploadResponse>;
|
|
27
43
|
/**
|
|
28
44
|
* Delete an image by its filename
|
|
29
45
|
* @param filename The exact filename (e.g., 'img_123.jpg')
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,13 @@ exports.VampireImageClient = void 0;
|
|
|
4
4
|
class VampireImageClient {
|
|
5
5
|
apiKey;
|
|
6
6
|
baseUrl;
|
|
7
|
+
timeoutMs;
|
|
8
|
+
maxRetries;
|
|
7
9
|
constructor(options) {
|
|
8
10
|
this.apiKey = options.apiKey;
|
|
9
11
|
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
12
|
+
this.timeoutMs = options.timeoutMs || 30000;
|
|
13
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
10
14
|
}
|
|
11
15
|
getHeaders(isFormData = false) {
|
|
12
16
|
const headers = {
|
|
@@ -17,58 +21,130 @@ class VampireImageClient {
|
|
|
17
21
|
}
|
|
18
22
|
return headers;
|
|
19
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Internal fetch wrapper with timeout and retry logic
|
|
26
|
+
*/
|
|
27
|
+
async fetchWithRetry(url, options, retries) {
|
|
28
|
+
let lastError = new Error('Request failed');
|
|
29
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
34
|
+
clearTimeout(timeoutId);
|
|
35
|
+
if (res.ok || (res.status >= 400 && res.status < 500)) {
|
|
36
|
+
// Successful response or client error (no need to retry 4xx errors usually)
|
|
37
|
+
return res;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Server responded with status ${res.status}`);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
clearTimeout(timeoutId);
|
|
43
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
44
|
+
// If it's an abort error (timeout), or network error, we retry.
|
|
45
|
+
if (attempt < retries) {
|
|
46
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 1s, 2s, 4s...
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw lastError;
|
|
52
|
+
}
|
|
20
53
|
/**
|
|
21
54
|
* Upload an image to the PHP server.
|
|
22
|
-
* @param file A standard File
|
|
23
|
-
* @param filename Optional filename (
|
|
55
|
+
* @param file A standard File, Blob, Buffer, or Uint8Array.
|
|
56
|
+
* @param filename Optional filename (required if passing a Buffer/Uint8Array)
|
|
24
57
|
*/
|
|
25
58
|
async uploadImage(file, filename) {
|
|
26
59
|
const formData = new FormData();
|
|
60
|
+
let blobToUpload;
|
|
61
|
+
if (file instanceof Uint8Array) {
|
|
62
|
+
blobToUpload = new Blob([file]);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
blobToUpload = file;
|
|
66
|
+
}
|
|
27
67
|
if (filename) {
|
|
28
|
-
formData.append('image',
|
|
68
|
+
formData.append('image', blobToUpload, filename);
|
|
29
69
|
}
|
|
30
70
|
else {
|
|
31
|
-
formData.append('image',
|
|
71
|
+
formData.append('image', blobToUpload);
|
|
32
72
|
}
|
|
33
73
|
try {
|
|
34
|
-
const response = await
|
|
74
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/upload`, {
|
|
35
75
|
method: 'POST',
|
|
36
76
|
headers: this.getHeaders(true),
|
|
37
77
|
body: formData
|
|
38
|
-
});
|
|
78
|
+
}, this.maxRetries);
|
|
39
79
|
if (!response.ok) {
|
|
40
80
|
const text = await response.text();
|
|
41
81
|
throw new Error(`Upload failed with status ${response.status}: ${text}`);
|
|
42
82
|
}
|
|
43
83
|
const data = await response.json();
|
|
44
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
success: data.success || false,
|
|
86
|
+
url: data.url,
|
|
87
|
+
filename: data.filename,
|
|
88
|
+
error: data.error
|
|
89
|
+
};
|
|
45
90
|
}
|
|
46
91
|
catch (error) {
|
|
47
92
|
return {
|
|
93
|
+
success: false,
|
|
48
94
|
error: error instanceof Error ? error.message : String(error)
|
|
49
95
|
};
|
|
50
96
|
}
|
|
51
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Upload a Base64 encoded image string to the PHP server.
|
|
100
|
+
* @param base64String The base64 string (can include the data URI prefix like "data:image/png;base64,...")
|
|
101
|
+
* @param filename The filename to save it as
|
|
102
|
+
*/
|
|
103
|
+
async uploadBase64(base64String, filename) {
|
|
104
|
+
// Strip data prefix if present
|
|
105
|
+
const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '');
|
|
106
|
+
try {
|
|
107
|
+
// Convert base64 to Uint8Array (works in Browser and Node)
|
|
108
|
+
const binaryString = atob(base64Data);
|
|
109
|
+
const len = binaryString.length;
|
|
110
|
+
const bytes = new Uint8Array(len);
|
|
111
|
+
for (let i = 0; i < len; i++) {
|
|
112
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
113
|
+
}
|
|
114
|
+
return await this.uploadImage(bytes, filename);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: `Failed to process base64 string: ${error instanceof Error ? error.message : String(error)}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
52
123
|
/**
|
|
53
124
|
* Delete an image by its filename
|
|
54
125
|
* @param filename The exact filename (e.g., 'img_123.jpg')
|
|
55
126
|
*/
|
|
56
127
|
async deleteImage(filename) {
|
|
57
128
|
try {
|
|
58
|
-
const response = await
|
|
129
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/delete`, {
|
|
59
130
|
method: 'DELETE',
|
|
60
131
|
headers: this.getHeaders(),
|
|
61
132
|
body: JSON.stringify({ filename })
|
|
62
|
-
});
|
|
133
|
+
}, this.maxRetries);
|
|
63
134
|
if (!response.ok) {
|
|
64
135
|
const text = await response.text();
|
|
65
136
|
throw new Error(`Delete failed with status ${response.status}: ${text}`);
|
|
66
137
|
}
|
|
67
138
|
const data = await response.json();
|
|
68
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
success: data.success || false,
|
|
141
|
+
message: data.message,
|
|
142
|
+
error: data.error
|
|
143
|
+
};
|
|
69
144
|
}
|
|
70
145
|
catch (error) {
|
|
71
146
|
return {
|
|
147
|
+
success: false,
|
|
72
148
|
error: error instanceof Error ? error.message : String(error)
|
|
73
149
|
};
|
|
74
150
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
export interface VampireImageClientOptions {
|
|
2
2
|
apiKey: string;
|
|
3
3
|
baseUrl: string;
|
|
4
|
+
/** Default timeout in milliseconds for requests (default: 30000) */
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
/** Maximum number of retries for failed network requests (default: 3) */
|
|
7
|
+
maxRetries?: number;
|
|
4
8
|
}
|
|
5
9
|
|
|
6
10
|
export interface UploadResponse {
|
|
7
|
-
success
|
|
11
|
+
success: boolean;
|
|
8
12
|
url?: string;
|
|
9
13
|
filename?: string;
|
|
10
14
|
error?: string;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export interface DeleteResponse {
|
|
14
|
-
success
|
|
18
|
+
success: boolean;
|
|
15
19
|
message?: string;
|
|
16
20
|
error?: string;
|
|
17
21
|
}
|
|
@@ -19,10 +23,14 @@ export interface DeleteResponse {
|
|
|
19
23
|
export class VampireImageClient {
|
|
20
24
|
private apiKey: string;
|
|
21
25
|
private baseUrl: string;
|
|
26
|
+
private timeoutMs: number;
|
|
27
|
+
private maxRetries: number;
|
|
22
28
|
|
|
23
29
|
constructor(options: VampireImageClientOptions) {
|
|
24
30
|
this.apiKey = options.apiKey;
|
|
25
31
|
this.baseUrl = options.baseUrl.replace(/\/$/, '');
|
|
32
|
+
this.timeoutMs = options.timeoutMs || 30000;
|
|
33
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
private getHeaders(isFormData = false): HeadersInit {
|
|
@@ -37,62 +45,143 @@ export class VampireImageClient {
|
|
|
37
45
|
return headers;
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Internal fetch wrapper with timeout and retry logic
|
|
50
|
+
*/
|
|
51
|
+
private async fetchWithRetry(url: string, options: RequestInit, retries: number): Promise<Response> {
|
|
52
|
+
let lastError: Error = new Error('Request failed');
|
|
53
|
+
|
|
54
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(url, { ...options, signal: controller.signal });
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
|
|
62
|
+
if (res.ok || (res.status >= 400 && res.status < 500)) {
|
|
63
|
+
// Successful response or client error (no need to retry 4xx errors usually)
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`Server responded with status ${res.status}`);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
71
|
+
|
|
72
|
+
// If it's an abort error (timeout), or network error, we retry.
|
|
73
|
+
if (attempt < retries) {
|
|
74
|
+
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff: 1s, 2s, 4s...
|
|
75
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw lastError;
|
|
81
|
+
}
|
|
82
|
+
|
|
40
83
|
/**
|
|
41
84
|
* Upload an image to the PHP server.
|
|
42
|
-
* @param file A standard File
|
|
43
|
-
* @param filename Optional filename (
|
|
85
|
+
* @param file A standard File, Blob, Buffer, or Uint8Array.
|
|
86
|
+
* @param filename Optional filename (required if passing a Buffer/Uint8Array)
|
|
44
87
|
*/
|
|
45
|
-
async uploadImage(file: Blob, filename?: string): Promise<UploadResponse> {
|
|
88
|
+
async uploadImage(file: Blob | Uint8Array, filename?: string): Promise<UploadResponse> {
|
|
46
89
|
const formData = new FormData();
|
|
47
90
|
|
|
91
|
+
let blobToUpload: Blob;
|
|
92
|
+
|
|
93
|
+
if (file instanceof Uint8Array) {
|
|
94
|
+
blobToUpload = new Blob([file as any]);
|
|
95
|
+
} else {
|
|
96
|
+
blobToUpload = file;
|
|
97
|
+
}
|
|
98
|
+
|
|
48
99
|
if (filename) {
|
|
49
|
-
formData.append('image',
|
|
100
|
+
formData.append('image', blobToUpload, filename);
|
|
50
101
|
} else {
|
|
51
|
-
formData.append('image',
|
|
102
|
+
formData.append('image', blobToUpload);
|
|
52
103
|
}
|
|
53
104
|
|
|
54
105
|
try {
|
|
55
|
-
const response = await
|
|
106
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/upload`, {
|
|
56
107
|
method: 'POST',
|
|
57
108
|
headers: this.getHeaders(true),
|
|
58
109
|
body: formData
|
|
59
|
-
});
|
|
110
|
+
}, this.maxRetries);
|
|
60
111
|
|
|
61
112
|
if (!response.ok) {
|
|
62
113
|
const text = await response.text();
|
|
63
114
|
throw new Error(`Upload failed with status ${response.status}: ${text}`);
|
|
64
115
|
}
|
|
65
116
|
|
|
66
|
-
const data:
|
|
67
|
-
return
|
|
117
|
+
const data: any = await response.json();
|
|
118
|
+
return {
|
|
119
|
+
success: data.success || false,
|
|
120
|
+
url: data.url,
|
|
121
|
+
filename: data.filename,
|
|
122
|
+
error: data.error
|
|
123
|
+
};
|
|
68
124
|
} catch (error) {
|
|
69
125
|
return {
|
|
126
|
+
success: false,
|
|
70
127
|
error: error instanceof Error ? error.message : String(error)
|
|
71
128
|
};
|
|
72
129
|
}
|
|
73
130
|
}
|
|
74
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Upload a Base64 encoded image string to the PHP server.
|
|
134
|
+
* @param base64String The base64 string (can include the data URI prefix like "data:image/png;base64,...")
|
|
135
|
+
* @param filename The filename to save it as
|
|
136
|
+
*/
|
|
137
|
+
async uploadBase64(base64String: string, filename: string): Promise<UploadResponse> {
|
|
138
|
+
// Strip data prefix if present
|
|
139
|
+
const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '');
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Convert base64 to Uint8Array (works in Browser and Node)
|
|
143
|
+
const binaryString = atob(base64Data);
|
|
144
|
+
const len = binaryString.length;
|
|
145
|
+
const bytes = new Uint8Array(len);
|
|
146
|
+
for (let i = 0; i < len; i++) {
|
|
147
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return await this.uploadImage(bytes, filename);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
error: `Failed to process base64 string: ${error instanceof Error ? error.message : String(error)}`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
75
159
|
/**
|
|
76
160
|
* Delete an image by its filename
|
|
77
161
|
* @param filename The exact filename (e.g., 'img_123.jpg')
|
|
78
162
|
*/
|
|
79
163
|
async deleteImage(filename: string): Promise<DeleteResponse> {
|
|
80
164
|
try {
|
|
81
|
-
const response = await
|
|
165
|
+
const response = await this.fetchWithRetry(`${this.baseUrl}/delete`, {
|
|
82
166
|
method: 'DELETE',
|
|
83
167
|
headers: this.getHeaders(),
|
|
84
168
|
body: JSON.stringify({ filename })
|
|
85
|
-
});
|
|
169
|
+
}, this.maxRetries);
|
|
86
170
|
|
|
87
171
|
if (!response.ok) {
|
|
88
172
|
const text = await response.text();
|
|
89
173
|
throw new Error(`Delete failed with status ${response.status}: ${text}`);
|
|
90
174
|
}
|
|
91
175
|
|
|
92
|
-
const data:
|
|
93
|
-
return
|
|
176
|
+
const data: any = await response.json();
|
|
177
|
+
return {
|
|
178
|
+
success: data.success || false,
|
|
179
|
+
message: data.message,
|
|
180
|
+
error: data.error
|
|
181
|
+
};
|
|
94
182
|
} catch (error) {
|
|
95
183
|
return {
|
|
184
|
+
success: false,
|
|
96
185
|
error: error instanceof Error ? error.message : String(error)
|
|
97
186
|
};
|
|
98
187
|
}
|