@12-apps/storage 1.0.0 → 2.0.1
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/package.json +4 -4
- package/src/react/failures.ts +70 -10
- package/src/react/use-upload.ts +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/storage",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Image uploads that land at YOUR OWN ORIGIN, never at the bucket (12-20). Isomorphic core (format allowlist, upload ceiling, object-key grammar, rendition set, refusal taxonomy, wire schemas), host-mounted backend surface (./server: createApiStorage, the local-disk driver, the image-pipeline port, the orphan reclaim), Hono adapter (./hono), the S3-compatible driver behind its own subpath (./s3) and the bound picker + upload hook (./react). Standardized adoption contract in ADOPTING.md.",
|
|
6
6
|
"exports": {
|
|
@@ -40,12 +40,12 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@12-apps/ui": "^
|
|
43
|
+
"@12-apps/ui": "^6.1.0",
|
|
44
44
|
"zod": "^4.3.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@12-apps/eslint-config": "^1.
|
|
48
|
-
"@12-apps/typescript-config": "^1.20.
|
|
47
|
+
"@12-apps/eslint-config": "^1.21.1",
|
|
48
|
+
"@12-apps/typescript-config": "^1.20.1",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.703.0",
|
|
50
50
|
"@testing-library/react": "^16.1.0",
|
|
51
51
|
"@types/react": "19.2.2",
|
package/src/react/failures.ts
CHANGED
|
@@ -58,6 +58,38 @@ export interface WebStorageMessages {
|
|
|
58
58
|
session_expired: string;
|
|
59
59
|
/** `fetch` rejected outright; no response ever arrived. */
|
|
60
60
|
transport: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* THE FIVE BELOW ARE THE REFUSALS THAT USED TO HAVE NO KNOB AT ALL.
|
|
64
|
+
*
|
|
65
|
+
* They are decided in the browser before a request leaves it (see
|
|
66
|
+
* {@link rejectFileUpfront}) or after a response nothing could parse, and they
|
|
67
|
+
* were written as literals inside those functions — not defaults a host could
|
|
68
|
+
* override, but sentences a host could not reach. The docstring at the top of
|
|
69
|
+
* this file already argued they should be here ("every sentence here reaches a
|
|
70
|
+
* store owner, so it is product copy"); these four simply never arrived.
|
|
71
|
+
*
|
|
72
|
+
* A host overriding `file_too_large` and finding its own words replaced two
|
|
73
|
+
* lines later by ours is the specific failure this closes.
|
|
74
|
+
*
|
|
75
|
+
* Three take the fact they are about, because the sentence is worthless
|
|
76
|
+
* without it: the size that was refused, the type that was refused. Functions
|
|
77
|
+
* rather than tokens in a string, so a host writing "8,4 MB é grande demais"
|
|
78
|
+
* can put the number where its own grammar wants it.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/** A 0-byte file — an interrupted copy or an unreadable share. */
|
|
82
|
+
empty_file: string;
|
|
83
|
+
/** Over the ceiling, before any request. Both sizes already formatted. */
|
|
84
|
+
file_too_large_upfront: (context: { size: string; limit: string }) => string;
|
|
85
|
+
/** The OS identified no type at all — an extensionless drop, most often. */
|
|
86
|
+
unknown_content_type: string;
|
|
87
|
+
/** A type we do not accept. Carries it, because the owner will dispute it. */
|
|
88
|
+
unsupported_content_type_upfront: (context: { contentType: string }) => string;
|
|
89
|
+
/** 2xx with no key in the body — accepted, and unusable. */
|
|
90
|
+
missing_key: string;
|
|
91
|
+
/** Anything thrown on the way. The last resort, and it says so. */
|
|
92
|
+
upload_failed: string;
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
/** Copy that only makes sense with the configured ceiling in it. */
|
|
@@ -82,16 +114,34 @@ export function defaultWebStorageMessages(
|
|
|
82
114
|
not_found: 'Essa imagem não está mais disponível.',
|
|
83
115
|
session_expired: 'Sua sessão expirou. Entre novamente e repita o envio.',
|
|
84
116
|
transport: 'Não foi possível falar com o servidor. Verifique sua conexão e tente de novo.',
|
|
117
|
+
empty_file: 'Arquivo vazio (0 bytes). Escolha a imagem novamente.',
|
|
118
|
+
file_too_large_upfront: ({ size, limit }) =>
|
|
119
|
+
`Imagem muito grande: ${size}. O limite é ${limit} — reduza a imagem e tente de novo.`,
|
|
120
|
+
unknown_content_type:
|
|
121
|
+
'Não foi possível identificar o tipo do arquivo. Envie um PNG, JPG ou WebP.',
|
|
122
|
+
unsupported_content_type_upfront: ({ contentType }) =>
|
|
123
|
+
`Formato não suportado (${contentType}). Envie PNG, JPG, WebP ou GIF.`,
|
|
124
|
+
missing_key: 'O servidor aceitou a imagem mas não devolveu a chave. Tente de novo.',
|
|
125
|
+
upload_failed: 'Falha ao enviar a imagem. Tente de novo em instantes.',
|
|
85
126
|
};
|
|
86
127
|
}
|
|
87
128
|
|
|
88
|
-
|
|
129
|
+
/**
|
|
130
|
+
* The messages in force for a mount's ceiling.
|
|
131
|
+
*
|
|
132
|
+
* Exported because `use-upload` needs the same table for the two refusals it
|
|
133
|
+
* phrases itself, and a second `{ ...defaults, ...overrides }` there would be
|
|
134
|
+
* the exact drift this file exists to prevent.
|
|
135
|
+
*/
|
|
136
|
+
export function messagesOf(
|
|
89
137
|
maxBytes: number,
|
|
90
|
-
overrides
|
|
138
|
+
overrides?: Partial<WebStorageMessages>,
|
|
91
139
|
): WebStorageMessages {
|
|
92
140
|
return { ...defaultWebStorageMessages({ limit: megabytes(maxBytes) }), ...overrides };
|
|
93
141
|
}
|
|
94
142
|
|
|
143
|
+
const resolve = messagesOf;
|
|
144
|
+
|
|
95
145
|
/**
|
|
96
146
|
* The sentence for a refusal CODE, or `undefined` for a code with no entry.
|
|
97
147
|
*
|
|
@@ -101,7 +151,11 @@ function resolve(
|
|
|
101
151
|
*/
|
|
102
152
|
function sentenceFor(messages: WebStorageMessages, code: string): string | undefined {
|
|
103
153
|
if (!Object.prototype.hasOwnProperty.call(messages, code)) return undefined;
|
|
104
|
-
|
|
154
|
+
const entry = messages[code as keyof WebStorageMessages];
|
|
155
|
+
// Only the plain-string entries are reachable by a server CODE. The three
|
|
156
|
+
// that take a fact are browser-side refusals with no server equivalent, and
|
|
157
|
+
// a response naming one would otherwise render a function at a store owner.
|
|
158
|
+
return typeof entry === 'string' ? entry : undefined;
|
|
105
159
|
}
|
|
106
160
|
|
|
107
161
|
/**
|
|
@@ -116,20 +170,26 @@ function sentenceFor(messages: WebStorageMessages, code: string): string | undef
|
|
|
116
170
|
* extensionless drop, most often) — worth its own sentence, because "formato não
|
|
117
171
|
* suportado" reads as an accusation about a file the owner knows is a PNG.
|
|
118
172
|
*/
|
|
119
|
-
export function rejectFileUpfront(
|
|
173
|
+
export function rejectFileUpfront(
|
|
174
|
+
file: File,
|
|
175
|
+
maxBytes: number,
|
|
176
|
+
overrides?: Partial<WebStorageMessages>,
|
|
177
|
+
): string | null {
|
|
178
|
+
const messages = resolve(maxBytes, overrides);
|
|
120
179
|
if (file.size === 0) {
|
|
121
|
-
return
|
|
180
|
+
return messages.empty_file;
|
|
122
181
|
}
|
|
123
182
|
if (file.size > maxBytes) {
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
|
|
183
|
+
return messages.file_too_large_upfront({
|
|
184
|
+
size: megabytes(file.size),
|
|
185
|
+
limit: megabytes(maxBytes),
|
|
186
|
+
});
|
|
127
187
|
}
|
|
128
188
|
if (file.type === '') {
|
|
129
|
-
return
|
|
189
|
+
return messages.unknown_content_type;
|
|
130
190
|
}
|
|
131
191
|
if (!ACCEPTED_CONTENT_TYPES.includes(file.type)) {
|
|
132
|
-
return
|
|
192
|
+
return messages.unsupported_content_type_upfront({ contentType: file.type });
|
|
133
193
|
}
|
|
134
194
|
return null;
|
|
135
195
|
}
|
package/src/react/use-upload.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { useState } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { STORAGE_PATHS } from '../paths';
|
|
4
4
|
import {
|
|
5
|
+
messagesOf,
|
|
5
6
|
rejectFileUpfront,
|
|
6
7
|
transportFailure,
|
|
7
8
|
uploadFailure,
|
|
@@ -87,7 +88,7 @@ export function useUpload(config: UseUploadConfig): UploadState {
|
|
|
87
88
|
const payload = (await response.json()) as { data?: { imageKey?: string } };
|
|
88
89
|
const key = payload.data?.imageKey;
|
|
89
90
|
if (!key) {
|
|
90
|
-
setError(
|
|
91
|
+
setError(messagesOf(config.maxBytes, config.messages).missing_key);
|
|
91
92
|
return null;
|
|
92
93
|
}
|
|
93
94
|
return key;
|
|
@@ -103,14 +104,14 @@ export function useUpload(config: UseUploadConfig): UploadState {
|
|
|
103
104
|
const upright =
|
|
104
105
|
options.optimize === false ? file : await optimizeImage(file, config.profile);
|
|
105
106
|
|
|
106
|
-
const upfront = rejectFileUpfront(upright, config.maxBytes);
|
|
107
|
+
const upfront = rejectFileUpfront(upright, config.maxBytes, config.messages);
|
|
107
108
|
if (upfront) {
|
|
108
109
|
setError(upfront);
|
|
109
110
|
return null;
|
|
110
111
|
}
|
|
111
112
|
return await send(upright);
|
|
112
113
|
} catch {
|
|
113
|
-
setError(
|
|
114
|
+
setError(messagesOf(config.maxBytes, config.messages).upload_failed);
|
|
114
115
|
return null;
|
|
115
116
|
} finally {
|
|
116
117
|
setUploading(false);
|