This commit is contained in:
Mantano 2026-04-28 11:14:11 +07:00
parent a424f3e48c
commit d384fb2429
6 changed files with 72 additions and 5 deletions

View file

@ -29,6 +29,8 @@ pnpm run dev
### Production build ### Production build
Use this when verifying changes.
```bash ```bash
pnpm run build pnpm run build
``` ```

View file

@ -1,7 +1,7 @@
{ {
"id": "come-down", "id": "come-down",
"name": "Come Down", "name": "Come Down",
"version": "1.1.1-rc.1", "version": "1.1.1",
"minAppVersion": "1.8.0", "minAppVersion": "1.8.0",
"description": "Maintains a cache of your notes embedded external images.", "description": "Maintains a cache of your notes embedded external images.",
"author": "mntno", "author": "mntno",

View file

@ -108,6 +108,7 @@ export const Env = {
SPACE: " ", SPACE: " ",
is: (value: unknown): value is string => typeof value === "string", is: (value: unknown): value is string => typeof value === "string",
nonEmpty: (value: unknown): string | undefined => typeof value === "string" && value !== "" ? value : undefined, nonEmpty: (value: unknown): string | undefined => typeof value === "string" && value !== "" ? value : undefined,
isNonEmpty: (value: unknown): value is string => typeof value === "string" && value !== "",
} as const, } as const,
bool: { bool: {

View file

@ -891,12 +891,15 @@ export class CacheManager {
const response = await requestUrl({ url: sourceUrl, method: 'GET', throw: false }); const response = await requestUrl({ url: sourceUrl, method: 'GET', throw: false });
const headers = Url.normalizeHeaders(response.headers); const headers = Url.normalizeHeaders(response.headers);
const contentType = headers[Url.RESPONSE_HEADER_LOWERCASE.contentType] ?? undefined; const contentType = headers[Url.RESPONSE_HEADER_LOWERCASE.contentType];
const cacheControl = headers[Url.RESPONSE_HEADER_LOWERCASE.cacheControl] ?? undefined; const cacheControl = headers[Url.RESPONSE_HEADER_LOWERCASE.cacheControl];
const etag = Url.parseETag(headers[Url.RESPONSE_HEADER_LOWERCASE.etag]);
const lastModifiedHeader = headers[Url.RESPONSE_HEADER_LOWERCASE.lastModified];
const lastModified = lastModifiedHeader !== undefined ? new Date(lastModifiedHeader).toISOString() : undefined;
//Env.log.cm(Env.dev.icon.CACHE_MANAGER, `CacheManager:download: Got response:\n\tcacheID: ${cacheKey}\n\t${response.status}\n\tcontentType: ${contentType}`); //Env.log.cm(Env.dev.icon.CACHE_MANAGER, `CacheManager:download: Got response:\n\tcacheID: ${cacheKey}\n\t${response.status}\n\tcontentType: ${contentType}`);
if (cacheControl == Url.CACHE_CONTROL_LOWERCASE.noStore) if (cacheControl === Url.CACHE_CONTROL_LOWERCASE.noStore)
throw new CacheError(`Caching not allowed on ${sourceUrl}.`, cacheKey); throw new CacheError(`Caching not allowed on ${sourceUrl}.`, cacheKey);
if (response.status >= 400) if (response.status >= 400)
@ -926,6 +929,8 @@ export class CacheManager {
d: nowDateString, d: nowDateString,
l: nowDateString, l: nowDateString,
cc: cacheControl, cc: cacheControl,
et: etag !== null ? etag : undefined,
m: lastModified,
}, },
f: { f: {
s: sourceUrl, s: sourceUrl,

View file

@ -75,9 +75,32 @@ export interface CacheMetadataTime {
/** Download time */ /** Download time */
d: string; d: string;
/** Last accessed */ /**
* "Last time checked".
*
* Currently set to download time and never updated.
* @todo May be used to keep track of when the server was last checked for modified.
*/
l: string; l: string;
/** Value of the `Cache-Control` HTTP response header. */ /** Value of the `Cache-Control` HTTP response header. */
cc?: string; cc?: string;
/**
* Value of the `ETag` HTTP response header.
*
* In the HTTP spec (RFC 7232), an ETag must be wrapped in double quotes. But here they are not. Add them back when using the ETag in requests.
*
* @since 1.1.1
*/
et?: string;
/**
* Normalized value of the `Last-Modified` HTTP response header (ISO 8601).
*
* Converted from HTTP-date format to ISO for consistency. Convert back to UTC string when using in 'If-Modified-Since' headers.
*
* @since 1.1.1
*/
m?: string;
} }

View file

@ -10,6 +10,8 @@ export class Url {
contentLength: "Content-Length".toLowerCase(), contentLength: "Content-Length".toLowerCase(),
cacheControl: "Cache-Control".toLowerCase(), cacheControl: "Cache-Control".toLowerCase(),
expires: "Expires".toLowerCase(), expires: "Expires".toLowerCase(),
etag: "ETag".toLowerCase(),
lastModified: "Last-Modified".toLowerCase(),
} as const; } as const;
public static readonly CACHE_CONTROL_LOWERCASE = { public static readonly CACHE_CONTROL_LOWERCASE = {
@ -81,6 +83,40 @@ export class Url {
return url.startsWith("app://") || url.startsWith("capacitor://") || url.startsWith("file://"); return url.startsWith("app://") || url.startsWith("capacitor://") || url.startsWith("file://");
} }
/**
* Parses an ETag header value into a storage-friendly format.
* - `W/"abc"` -> `W/abc`
* - `"abc"` -> `abc`
* @param value The raw ETag header string.
* @returns The tag value stripped of quotes, with "W/" prefix preserved if present.
* @since 1.1.1
*/
public static parseETag(value: string | undefined): string | null {
if (!Env.str.isNonEmpty(value))
return null;
const match = value.match(/^(W\/)?"(.*)"$/);
if (match === null)
return null;
const prefix = match[1] !== undefined ? match[1] : Env.str.EMPTY;
const tag = match[2];
return `${prefix}${tag}`;
}
/**
* Converts a stored ETag back to a format suitable for HTTP headers.
* - `W/abc` -> `W/"abc"`
* - `abc` -> `"abc"`
* @param tag The stored ETag value.
* @since 1.1.1
*/
public static stringifyETag(tag: string): string | null {
if (Env.str.nonEmpty(tag) === undefined)
return null;
return tag.replace(/^(W\/)?(.*)$/, '$1"$2"');
}
public static trimBackslash(url: string): string { public static trimBackslash(url: string): string {
return url.endsWith("/") ? url.slice(0, -1) : url; return url.endsWith("/") ? url.slice(0, -1) : url;
} }