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 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. Delete an Image
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?: boolean;
10
+ success: boolean;
7
11
  url?: string;
8
12
  filename?: string;
9
13
  error?: string;
10
14
  }
11
15
  export interface DeleteResponse {
12
- success?: boolean;
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 or Blob object (from browser)
24
- * @param filename Optional filename (mainly for Node.js usage where Blob might not have a name)
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
- uploadImage(file: Blob, filename?: string): Promise<UploadResponse>;
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 or Blob object (from browser)
23
- * @param filename Optional filename (mainly for Node.js usage where Blob might not have a name)
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', file, filename);
68
+ formData.append('image', blobToUpload, filename);
29
69
  }
30
70
  else {
31
- formData.append('image', file);
71
+ formData.append('image', blobToUpload);
32
72
  }
33
73
  try {
34
- const response = await fetch(`${this.baseUrl}/upload`, {
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 data;
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 fetch(`${this.baseUrl}/delete`, {
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 data;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikes-php-image-handler",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A Node.js client for uploading and managing images on a custom PHP image server.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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?: boolean;
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?: boolean;
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 or Blob object (from browser)
43
- * @param filename Optional filename (mainly for Node.js usage where Blob might not have a name)
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', file, filename);
100
+ formData.append('image', blobToUpload, filename);
50
101
  } else {
51
- formData.append('image', file);
102
+ formData.append('image', blobToUpload);
52
103
  }
53
104
 
54
105
  try {
55
- const response = await fetch(`${this.baseUrl}/upload`, {
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: UploadResponse = await response.json();
67
- return data;
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 fetch(`${this.baseUrl}/delete`, {
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: DeleteResponse = await response.json();
93
- return data;
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
  }