Compare commits

...

17 Commits

Author SHA1 Message Date
Nicolas Meienberger
3ff6a04f8e feat(repositories): allow importing existing repos 2025-11-15 11:58:52 +01:00
Nicolas Meienberger
54ee02deb9 feat(backups): manual repository cleanup 2025-11-15 11:24:17 +01:00
Nicolas Meienberger
b83881c189 fix(backups): re-calculate next backup date before starting the backup 2025-11-15 11:13:23 +01:00
Nicolas Meienberger
d78b4adfd9 chore: update readme 2025-11-15 10:37:35 +01:00
Nicolas Meienberger
4d3ec524e2 chore: add all caps for dev container 2025-11-15 10:23:15 +01:00
Nicolas Meienberger
681cf5dff1 fix: hide test-connection button for directories 2025-11-15 10:15:25 +01:00
Nicolas Meienberger
31da747c2d fix: mount and unmount command not properly throwing errors 2025-11-15 10:08:16 +01:00
Nicolas Meienberger
b86081b2e8 Merge branch 'altendorfme-backup-file-path' 2025-11-15 09:51:05 +01:00
Nicolas Meienberger
3622fd57ef refactor(repository): keep the error if repo is already init 2025-11-15 09:45:04 +01:00
Nicolas Meienberger
5b1d7eff17 chore: update .gitignore 2025-11-15 09:45:04 +01:00
Nicolas Meienberger
2b3d8dffc5 Merge branch 'altendorfme-main' 2025-11-15 09:42:50 +01:00
Nicolas Meienberger
f517438a8e refactor(repository): keep the error if repo is already init 2025-11-15 09:42:29 +01:00
Nicolas Meienberger
1ddd4d701b chore: update .gitignore 2025-11-15 09:39:49 +01:00
Renan Bernordi
9a1797b8b2 backup file and folders 2025-11-14 23:21:13 -03:00
Renan Bernordi
52046c88cc support cloudflare r2 2025-11-14 22:37:27 -03:00
Nicolas Meienberger
951d9d970c chore: update readme 2025-11-14 22:44:58 +01:00
Nicolas Meienberger
ffc821af2b chore: update readme 2025-11-14 21:36:33 +01:00
34 changed files with 4142 additions and 3968 deletions

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@
.env .env
.turbo .turbo
CLAUDE.md CLAUDE.md
mutagen.yml.lock
notes.md

View File

@@ -36,10 +36,11 @@ In order to run Ironmount, you need to have Docker and Docker Compose installed
```yaml ```yaml
services: services:
ironmount: ironmount:
image: ghcr.io/nicotsx/ironmount:v0.6 image: ghcr.io/nicotsx/ironmount:v0.8
container_name: ironmount container_name: ironmount
restart: unless-stopped restart: unless-stopped
privileged: true cap_add:
- SYS_ADMIN
ports: ports:
- "4096:4096" - "4096:4096"
devices: devices:
@@ -67,7 +68,7 @@ If you want to track a local directory on the same server where Ironmount is run
```diff ```diff
services: services:
ironmount: ironmount:
image: ghcr.io/nicotsx/ironmount:v0.6 image: ghcr.io/nicotsx/ironmount:v0.8
container_name: ironmount container_name: ironmount
restart: unless-stopped restart: unless-stopped
cap_add: cap_add:
@@ -132,10 +133,11 @@ Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage pro
```diff ```diff
services: services:
ironmount: ironmount:
image: ghcr.io/nicotsx/ironmount:v0.6 image: ghcr.io/nicotsx/ironmount:v0.8
container_name: ironmount container_name: ironmount
restart: unless-stopped restart: unless-stopped
privileged: true cap_add:
- SYS_ADMIN
ports: ports:
- "4096:4096" - "4096:4096"
devices: devices:
@@ -187,7 +189,7 @@ In order to enable this feature, you need to change your bind mount `/var/lib/ir
```diff ```diff
services: services:
ironmount: ironmount:
image: ghcr.io/nicotsx/ironmount:v0.6 image: ghcr.io/nicotsx/ironmount:v0.8
container_name: ironmount container_name: ironmount
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -215,7 +217,7 @@ In order to enable this feature, you need to run Ironmount with several items sh
```diff ```diff
services: services:
ironmount: ironmount:
image: ghcr.io/nicotsx/ironmount:v0.6 image: ghcr.io/nicotsx/ironmount:v0.8
container_name: ironmount container_name: ironmount
restart: unless-stopped restart: unless-stopped
cap_add: cap_add:

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import { type ClientOptions, type Config, createClient, createConfig } from "./client"; import { type ClientOptions, type Config, createClient, createConfig } from './client';
import type { ClientOptions as ClientOptions2 } from "./types.gen"; import type { ClientOptions as ClientOptions2 } from './types.gen';
/** /**
* The `createClientConfig()` function will be called on client initialization * The `createClientConfig()` function will be called on client initialization
@@ -11,12 +11,8 @@ import type { ClientOptions as ClientOptions2 } from "./types.gen";
* `setConfig()`. This is useful for example if you're using Next.js * `setConfig()`. This is useful for example if you're using Next.js
* to ensure your client always has the correct values. * to ensure your client always has the correct values.
*/ */
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = ( export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>;
export const client = createClient( export const client = createClient(createConfig<ClientOptions2>({
createConfig<ClientOptions2>({ baseUrl: 'http://192.168.2.42:4096'
baseUrl: "http://192.168.2.42:4096", }));
}),
);

View File

@@ -1,278 +1,301 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import { createSseClient } from "../core/serverSentEvents.gen"; import { createSseClient } from '../core/serverSentEvents.gen';
import type { HttpMethod } from "../core/types.gen"; import type { HttpMethod } from '../core/types.gen';
import { getValidRequestBody } from "../core/utils.gen"; import { getValidRequestBody } from '../core/utils.gen';
import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen"; import type {
Client,
Config,
RequestOptions,
ResolvedRequestOptions,
} from './types.gen';
import { import {
buildUrl, buildUrl,
createConfig, createConfig,
createInterceptors, createInterceptors,
getParseAs, getParseAs,
mergeConfigs, mergeConfigs,
mergeHeaders, mergeHeaders,
setAuthParams, setAuthParams,
} from "./utils.gen"; } from './utils.gen';
type ReqInit = Omit<RequestInit, "body" | "headers"> & { type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
body?: any; body?: any;
headers: ReturnType<typeof mergeHeaders>; headers: ReturnType<typeof mergeHeaders>;
}; };
export const createClient = (config: Config = {}): Client => { export const createClient = (config: Config = {}): Client => {
let _config = mergeConfigs(createConfig(), config); let _config = mergeConfigs(createConfig(), config);
const getConfig = (): Config => ({ ..._config }); const getConfig = (): Config => ({ ..._config });
const setConfig = (config: Config): Config => { const setConfig = (config: Config): Config => {
_config = mergeConfigs(_config, config); _config = mergeConfigs(_config, config);
return getConfig(); return getConfig();
}; };
const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>(); const interceptors = createInterceptors<
Request,
Response,
unknown,
ResolvedRequestOptions
>();
const beforeRequest = async (options: RequestOptions) => { const beforeRequest = async (options: RequestOptions) => {
const opts = { const opts = {
..._config, ..._config,
...options, ...options,
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
headers: mergeHeaders(_config.headers, options.headers), headers: mergeHeaders(_config.headers, options.headers),
serializedBody: undefined, serializedBody: undefined,
}; };
if (opts.security) { if (opts.security) {
await setAuthParams({ await setAuthParams({
...opts, ...opts,
security: opts.security, security: opts.security,
}); });
} }
if (opts.requestValidator) { if (opts.requestValidator) {
await opts.requestValidator(opts); await opts.requestValidator(opts);
} }
if (opts.body !== undefined && opts.bodySerializer) { if (opts.body !== undefined && opts.bodySerializer) {
opts.serializedBody = opts.bodySerializer(opts.body); opts.serializedBody = opts.bodySerializer(opts.body);
} }
// remove Content-Type header if body is empty to avoid sending invalid requests // remove Content-Type header if body is empty to avoid sending invalid requests
if (opts.body === undefined || opts.serializedBody === "") { if (opts.body === undefined || opts.serializedBody === '') {
opts.headers.delete("Content-Type"); opts.headers.delete('Content-Type');
} }
const url = buildUrl(opts); const url = buildUrl(opts);
return { opts, url }; return { opts, url };
}; };
const request: Client["request"] = async (options) => { const request: Client['request'] = async (options) => {
// @ts-expect-error // @ts-expect-error
const { opts, url } = await beforeRequest(options); const { opts, url } = await beforeRequest(options);
const requestInit: ReqInit = { const requestInit: ReqInit = {
redirect: "follow", redirect: 'follow',
...opts, ...opts,
body: getValidRequestBody(opts), body: getValidRequestBody(opts),
}; };
let request = new Request(url, requestInit); let request = new Request(url, requestInit);
for (const fn of interceptors.request.fns) { for (const fn of interceptors.request.fns) {
if (fn) { if (fn) {
request = await fn(request, opts); request = await fn(request, opts);
} }
} }
// fetch must be assigned here, otherwise it would throw the error: // fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = opts.fetch!; const _fetch = opts.fetch!;
let response: Response; let response: Response;
try { try {
response = await _fetch(request); response = await _fetch(request);
} catch (error) { } catch (error) {
// Handle fetch exceptions (AbortError, network errors, etc.) // Handle fetch exceptions (AbortError, network errors, etc.)
let finalError = error; let finalError = error;
for (const fn of interceptors.error.fns) { for (const fn of interceptors.error.fns) {
if (fn) { if (fn) {
finalError = (await fn(error, undefined as any, request, opts)) as unknown; finalError = (await fn(
} error,
} undefined as any,
request,
opts,
)) as unknown;
}
}
finalError = finalError || ({} as unknown); finalError = finalError || ({} as unknown);
if (opts.throwOnError) { if (opts.throwOnError) {
throw finalError; throw finalError;
} }
// Return error response // Return error response
return opts.responseStyle === "data" return opts.responseStyle === 'data'
? undefined ? undefined
: { : {
error: finalError, error: finalError,
request, request,
response: undefined as any, response: undefined as any,
}; };
} }
for (const fn of interceptors.response.fns) { for (const fn of interceptors.response.fns) {
if (fn) { if (fn) {
response = await fn(response, request, opts); response = await fn(response, request, opts);
} }
} }
const result = { const result = {
request, request,
response, response,
}; };
if (response.ok) { if (response.ok) {
const parseAs = const parseAs =
(opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json"; (opts.parseAs === 'auto'
? getParseAs(response.headers.get('Content-Type'))
: opts.parseAs) ?? 'json';
if (response.status === 204 || response.headers.get("Content-Length") === "0") { if (
let emptyData: any; response.status === 204 ||
switch (parseAs) { response.headers.get('Content-Length') === '0'
case "arrayBuffer": ) {
case "blob": let emptyData: any;
case "text": switch (parseAs) {
emptyData = await response[parseAs](); case 'arrayBuffer':
break; case 'blob':
case "formData": case 'text':
emptyData = new FormData(); emptyData = await response[parseAs]();
break; break;
case "stream": case 'formData':
emptyData = response.body; emptyData = new FormData();
break; break;
case "json": case 'stream':
default: emptyData = response.body;
emptyData = {}; break;
break; case 'json':
} default:
return opts.responseStyle === "data" emptyData = {};
? emptyData break;
: { }
data: emptyData, return opts.responseStyle === 'data'
...result, ? emptyData
}; : {
} data: emptyData,
...result,
};
}
let data: any; let data: any;
switch (parseAs) { switch (parseAs) {
case "arrayBuffer": case 'arrayBuffer':
case "blob": case 'blob':
case "formData": case 'formData':
case "json": case 'json':
case "text": case 'text':
data = await response[parseAs](); data = await response[parseAs]();
break; break;
case "stream": case 'stream':
return opts.responseStyle === "data" return opts.responseStyle === 'data'
? response.body ? response.body
: { : {
data: response.body, data: response.body,
...result, ...result,
}; };
} }
if (parseAs === "json") { if (parseAs === 'json') {
if (opts.responseValidator) { if (opts.responseValidator) {
await opts.responseValidator(data); await opts.responseValidator(data);
} }
if (opts.responseTransformer) { if (opts.responseTransformer) {
data = await opts.responseTransformer(data); data = await opts.responseTransformer(data);
} }
} }
return opts.responseStyle === "data" return opts.responseStyle === 'data'
? data ? data
: { : {
data, data,
...result, ...result,
}; };
} }
const textError = await response.text(); const textError = await response.text();
let jsonError: unknown; let jsonError: unknown;
try { try {
jsonError = JSON.parse(textError); jsonError = JSON.parse(textError);
} catch { } catch {
// noop // noop
} }
const error = jsonError ?? textError; const error = jsonError ?? textError;
let finalError = error; let finalError = error;
for (const fn of interceptors.error.fns) { for (const fn of interceptors.error.fns) {
if (fn) { if (fn) {
finalError = (await fn(error, response, request, opts)) as string; finalError = (await fn(error, response, request, opts)) as string;
} }
} }
finalError = finalError || ({} as string); finalError = finalError || ({} as string);
if (opts.throwOnError) { if (opts.throwOnError) {
throw finalError; throw finalError;
} }
// TODO: we probably want to return error and improve types // TODO: we probably want to return error and improve types
return opts.responseStyle === "data" return opts.responseStyle === 'data'
? undefined ? undefined
: { : {
error: finalError, error: finalError,
...result, ...result,
}; };
}; };
const makeMethodFn = (method: Uppercase<HttpMethod>) => (options: RequestOptions) => request({ ...options, method }); const makeMethodFn =
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
request({ ...options, method });
const makeSseFn = (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => { const makeSseFn =
const { opts, url } = await beforeRequest(options); (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
return createSseClient({ const { opts, url } = await beforeRequest(options);
...opts, return createSseClient({
body: opts.body as BodyInit | null | undefined, ...opts,
headers: opts.headers as unknown as Record<string, string>, body: opts.body as BodyInit | null | undefined,
method, headers: opts.headers as unknown as Record<string, string>,
onRequest: async (url, init) => { method,
let request = new Request(url, init); onRequest: async (url, init) => {
for (const fn of interceptors.request.fns) { let request = new Request(url, init);
if (fn) { for (const fn of interceptors.request.fns) {
request = await fn(request, opts); if (fn) {
} request = await fn(request, opts);
} }
return request; }
}, return request;
url, },
}); url,
}; });
};
return { return {
buildUrl, buildUrl,
connect: makeMethodFn("CONNECT"), connect: makeMethodFn('CONNECT'),
delete: makeMethodFn("DELETE"), delete: makeMethodFn('DELETE'),
get: makeMethodFn("GET"), get: makeMethodFn('GET'),
getConfig, getConfig,
head: makeMethodFn("HEAD"), head: makeMethodFn('HEAD'),
interceptors, interceptors,
options: makeMethodFn("OPTIONS"), options: makeMethodFn('OPTIONS'),
patch: makeMethodFn("PATCH"), patch: makeMethodFn('PATCH'),
post: makeMethodFn("POST"), post: makeMethodFn('POST'),
put: makeMethodFn("PUT"), put: makeMethodFn('PUT'),
request, request,
setConfig, setConfig,
sse: { sse: {
connect: makeSseFn("CONNECT"), connect: makeSseFn('CONNECT'),
delete: makeSseFn("DELETE"), delete: makeSseFn('DELETE'),
get: makeSseFn("GET"), get: makeSseFn('GET'),
head: makeSseFn("HEAD"), head: makeSseFn('HEAD'),
options: makeSseFn("OPTIONS"), options: makeSseFn('OPTIONS'),
patch: makeSseFn("PATCH"), patch: makeSseFn('PATCH'),
post: makeSseFn("POST"), post: makeSseFn('POST'),
put: makeSseFn("PUT"), put: makeSseFn('PUT'),
trace: makeSseFn("TRACE"), trace: makeSseFn('TRACE'),
}, },
trace: makeMethodFn("TRACE"), trace: makeMethodFn('TRACE'),
} as Client; } as Client;
}; };

View File

@@ -1,25 +1,25 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export type { Auth } from "../core/auth.gen"; export type { Auth } from '../core/auth.gen';
export type { QuerySerializerOptions } from "../core/bodySerializer.gen"; export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
export { export {
formDataBodySerializer, formDataBodySerializer,
jsonBodySerializer, jsonBodySerializer,
urlSearchParamsBodySerializer, urlSearchParamsBodySerializer,
} from "../core/bodySerializer.gen"; } from '../core/bodySerializer.gen';
export { buildClientParams } from "../core/params.gen"; export { buildClientParams } from '../core/params.gen';
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen"; export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
export { createClient } from "./client.gen"; export { createClient } from './client.gen';
export type { export type {
Client, Client,
ClientOptions, ClientOptions,
Config, Config,
CreateClientConfig, CreateClientConfig,
Options, Options,
RequestOptions, RequestOptions,
RequestResult, RequestResult,
ResolvedRequestOptions, ResolvedRequestOptions,
ResponseStyle, ResponseStyle,
TDataShape, TDataShape,
} from "./types.gen"; } from './types.gen';
export { createConfig, mergeHeaders } from "./utils.gen"; export { createConfig, mergeHeaders } from './utils.gen';

View File

@@ -1,174 +1,210 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Auth } from "../core/auth.gen"; import type { Auth } from '../core/auth.gen';
import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen"; import type {
import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen"; ServerSentEventsOptions,
import type { Middleware } from "./utils.gen"; ServerSentEventsResult,
} from '../core/serverSentEvents.gen';
import type {
Client as CoreClient,
Config as CoreConfig,
} from '../core/types.gen';
import type { Middleware } from './utils.gen';
export type ResponseStyle = "data" | "fields"; export type ResponseStyle = 'data' | 'fields';
export interface Config<T extends ClientOptions = ClientOptions> export interface Config<T extends ClientOptions = ClientOptions>
extends Omit<RequestInit, "body" | "headers" | "method">, extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
CoreConfig { CoreConfig {
/** /**
* Base URL for all requests made by this client. * Base URL for all requests made by this client.
*/ */
baseUrl?: T["baseUrl"]; baseUrl?: T['baseUrl'];
/** /**
* Fetch API implementation. You can use this option to provide a custom * Fetch API implementation. You can use this option to provide a custom
* fetch instance. * fetch instance.
* *
* @default globalThis.fetch * @default globalThis.fetch
*/ */
fetch?: typeof fetch; fetch?: typeof fetch;
/** /**
* Please don't use the Fetch client for Next.js applications. The `next` * Please don't use the Fetch client for Next.js applications. The `next`
* options won't have any effect. * options won't have any effect.
* *
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
*/ */
next?: never; next?: never;
/** /**
* Return the response data parsed in a specified format. By default, `auto` * Return the response data parsed in a specified format. By default, `auto`
* will infer the appropriate method from the `Content-Type` response header. * will infer the appropriate method from the `Content-Type` response header.
* You can override this behavior with any of the {@link Body} methods. * You can override this behavior with any of the {@link Body} methods.
* Select `stream` if you don't want to parse response data at all. * Select `stream` if you don't want to parse response data at all.
* *
* @default 'auto' * @default 'auto'
*/ */
parseAs?: "arrayBuffer" | "auto" | "blob" | "formData" | "json" | "stream" | "text"; parseAs?:
/** | 'arrayBuffer'
* Should we return only data or multiple fields (data, error, response, etc.)? | 'auto'
* | 'blob'
* @default 'fields' | 'formData'
*/ | 'json'
responseStyle?: ResponseStyle; | 'stream'
/** | 'text';
* Throw an error instead of returning it in the response? /**
* * Should we return only data or multiple fields (data, error, response, etc.)?
* @default false *
*/ * @default 'fields'
throwOnError?: T["throwOnError"]; */
responseStyle?: ResponseStyle;
/**
* Throw an error instead of returning it in the response?
*
* @default false
*/
throwOnError?: T['throwOnError'];
} }
export interface RequestOptions< export interface RequestOptions<
TData = unknown, TData = unknown,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
Url extends string = string, Url extends string = string,
> extends Config<{ > extends Config<{
responseStyle: TResponseStyle; responseStyle: TResponseStyle;
throwOnError: ThrowOnError; throwOnError: ThrowOnError;
}>, }>,
Pick< Pick<
ServerSentEventsOptions<TData>, ServerSentEventsOptions<TData>,
"onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay" | 'onSseError'
> { | 'onSseEvent'
/** | 'sseDefaultRetryDelay'
* Any body that you want to add to your request. | 'sseMaxRetryAttempts'
* | 'sseMaxRetryDelay'
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body} > {
*/ /**
body?: unknown; * Any body that you want to add to your request.
path?: Record<string, unknown>; *
query?: Record<string, unknown>; * {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
/** */
* Security mechanism(s) to use for the request. body?: unknown;
*/ path?: Record<string, unknown>;
security?: ReadonlyArray<Auth>; query?: Record<string, unknown>;
url: Url; /**
* Security mechanism(s) to use for the request.
*/
security?: ReadonlyArray<Auth>;
url: Url;
} }
export interface ResolvedRequestOptions< export interface ResolvedRequestOptions<
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
Url extends string = string, Url extends string = string,
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> { > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
serializedBody?: string; serializedBody?: string;
} }
export type RequestResult< export type RequestResult<
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
> = ThrowOnError extends true > = ThrowOnError extends true
? Promise< ? Promise<
TResponseStyle extends "data" TResponseStyle extends 'data'
? TData extends Record<string, unknown> ? TData extends Record<string, unknown>
? TData[keyof TData] ? TData[keyof TData]
: TData : TData
: { : {
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; data: TData extends Record<string, unknown>
request: Request; ? TData[keyof TData]
response: Response; : TData;
} request: Request;
> response: Response;
: Promise< }
TResponseStyle extends "data" >
? (TData extends Record<string, unknown> ? TData[keyof TData] : TData) | undefined : Promise<
: ( TResponseStyle extends 'data'
| { ?
data: TData extends Record<string, unknown> ? TData[keyof TData] : TData; | (TData extends Record<string, unknown>
error: undefined; ? TData[keyof TData]
} : TData)
| { | undefined
data: undefined; : (
error: TError extends Record<string, unknown> ? TError[keyof TError] : TError; | {
} data: TData extends Record<string, unknown>
) & { ? TData[keyof TData]
request: Request; : TData;
response: Response; error: undefined;
} }
>; | {
data: undefined;
error: TError extends Record<string, unknown>
? TError[keyof TError]
: TError;
}
) & {
request: Request;
response: Response;
}
>;
export interface ClientOptions { export interface ClientOptions {
baseUrl?: string; baseUrl?: string;
responseStyle?: ResponseStyle; responseStyle?: ResponseStyle;
throwOnError?: boolean; throwOnError?: boolean;
} }
type MethodFn = < type MethodFn = <
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
>( >(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">, options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type SseFn = < type SseFn = <
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
>( >(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">, options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
) => Promise<ServerSentEventsResult<TData, TError>>; ) => Promise<ServerSentEventsResult<TData, TError>>;
type RequestFn = < type RequestFn = <
TData = unknown, TData = unknown,
TError = unknown, TError = unknown,
ThrowOnError extends boolean = false, ThrowOnError extends boolean = false,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
>( >(
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> & options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">, Pick<
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
'method'
>,
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
type BuildUrlFn = < type BuildUrlFn = <
TData extends { TData extends {
body?: unknown; body?: unknown;
path?: Record<string, unknown>; path?: Record<string, unknown>;
query?: Record<string, unknown>; query?: Record<string, unknown>;
url: string; url: string;
}, },
>( >(
options: TData & Options<TData>, options: TData & Options<TData>,
) => string; ) => string;
export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn> & { export type Client = CoreClient<
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>; RequestFn,
Config,
MethodFn,
BuildUrlFn,
SseFn
> & {
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
}; };
/** /**
@@ -180,23 +216,26 @@ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn>
* to ensure your client always has the correct values. * to ensure your client always has the correct values.
*/ */
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>, override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>; ) => Config<Required<ClientOptions> & T>;
export interface TDataShape { export interface TDataShape {
body?: unknown; body?: unknown;
headers?: unknown; headers?: unknown;
path?: unknown; path?: unknown;
query?: unknown; query?: unknown;
url: string; url: string;
} }
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>; type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Options< export type Options<
TData extends TDataShape = TDataShape, TData extends TDataShape = TDataShape,
ThrowOnError extends boolean = boolean, ThrowOnError extends boolean = boolean,
TResponse = unknown, TResponse = unknown,
TResponseStyle extends ResponseStyle = "fields", TResponseStyle extends ResponseStyle = 'fields',
> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & > = OmitKeys<
([TData] extends [never] ? unknown : Omit<TData, "url">); RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
'body' | 'path' | 'query' | 'url'
> &
([TData] extends [never] ? unknown : Omit<TData, 'url'>);

View File

@@ -1,289 +1,332 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import { getAuthToken } from "../core/auth.gen"; import { getAuthToken } from '../core/auth.gen';
import type { QuerySerializerOptions } from "../core/bodySerializer.gen"; import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
import { jsonBodySerializer } from "../core/bodySerializer.gen"; import { jsonBodySerializer } from '../core/bodySerializer.gen';
import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen"; import {
import { getUrl } from "../core/utils.gen"; serializeArrayParam,
import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen"; serializeObjectParam,
serializePrimitiveParam,
} from '../core/pathSerializer.gen';
import { getUrl } from '../core/utils.gen';
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
export const createQuerySerializer = <T = unknown>({ parameters = {}, ...args }: QuerySerializerOptions = {}) => { export const createQuerySerializer = <T = unknown>({
const querySerializer = (queryParams: T) => { parameters = {},
const search: string[] = []; ...args
if (queryParams && typeof queryParams === "object") { }: QuerySerializerOptions = {}) => {
for (const name in queryParams) { const querySerializer = (queryParams: T) => {
const value = queryParams[name]; const search: string[] = [];
if (queryParams && typeof queryParams === 'object') {
for (const name in queryParams) {
const value = queryParams[name];
if (value === undefined || value === null) { if (value === undefined || value === null) {
continue; continue;
} }
const options = parameters[name] || args; const options = parameters[name] || args;
if (Array.isArray(value)) { if (Array.isArray(value)) {
const serializedArray = serializeArrayParam({ const serializedArray = serializeArrayParam({
allowReserved: options.allowReserved, allowReserved: options.allowReserved,
explode: true, explode: true,
name, name,
style: "form", style: 'form',
value, value,
...options.array, ...options.array,
}); });
if (serializedArray) search.push(serializedArray); if (serializedArray) search.push(serializedArray);
} else if (typeof value === "object") { } else if (typeof value === 'object') {
const serializedObject = serializeObjectParam({ const serializedObject = serializeObjectParam({
allowReserved: options.allowReserved, allowReserved: options.allowReserved,
explode: true, explode: true,
name, name,
style: "deepObject", style: 'deepObject',
value: value as Record<string, unknown>, value: value as Record<string, unknown>,
...options.object, ...options.object,
}); });
if (serializedObject) search.push(serializedObject); if (serializedObject) search.push(serializedObject);
} else { } else {
const serializedPrimitive = serializePrimitiveParam({ const serializedPrimitive = serializePrimitiveParam({
allowReserved: options.allowReserved, allowReserved: options.allowReserved,
name, name,
value: value as string, value: value as string,
}); });
if (serializedPrimitive) search.push(serializedPrimitive); if (serializedPrimitive) search.push(serializedPrimitive);
} }
} }
} }
return search.join("&"); return search.join('&');
}; };
return querySerializer; return querySerializer;
}; };
/** /**
* Infers parseAs value from provided Content-Type header. * Infers parseAs value from provided Content-Type header.
*/ */
export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"], "auto"> => { export const getParseAs = (
if (!contentType) { contentType: string | null,
// If no Content-Type header is provided, the best we can do is return the raw response body, ): Exclude<Config['parseAs'], 'auto'> => {
// which is effectively the same as the 'stream' option. if (!contentType) {
return "stream"; // If no Content-Type header is provided, the best we can do is return the raw response body,
} // which is effectively the same as the 'stream' option.
return 'stream';
}
const cleanContent = contentType.split(";")[0]?.trim(); const cleanContent = contentType.split(';')[0]?.trim();
if (!cleanContent) { if (!cleanContent) {
return; return;
} }
if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) { if (
return "json"; cleanContent.startsWith('application/json') ||
} cleanContent.endsWith('+json')
) {
return 'json';
}
if (cleanContent === "multipart/form-data") { if (cleanContent === 'multipart/form-data') {
return "formData"; return 'formData';
} }
if (["application/", "audio/", "image/", "video/"].some((type) => cleanContent.startsWith(type))) { if (
return "blob"; ['application/', 'audio/', 'image/', 'video/'].some((type) =>
} cleanContent.startsWith(type),
)
) {
return 'blob';
}
if (cleanContent.startsWith("text/")) { if (cleanContent.startsWith('text/')) {
return "text"; return 'text';
} }
return; return;
}; };
const checkForExistence = ( const checkForExistence = (
options: Pick<RequestOptions, "auth" | "query"> & { options: Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers; headers: Headers;
}, },
name?: string, name?: string,
): boolean => { ): boolean => {
if (!name) { if (!name) {
return false; return false;
} }
if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) { if (
return true; options.headers.has(name) ||
} options.query?.[name] ||
return false; options.headers.get('Cookie')?.includes(`${name}=`)
) {
return true;
}
return false;
}; };
export const setAuthParams = async ({ export const setAuthParams = async ({
security, security,
...options ...options
}: Pick<Required<RequestOptions>, "security"> & }: Pick<Required<RequestOptions>, 'security'> &
Pick<RequestOptions, "auth" | "query"> & { Pick<RequestOptions, 'auth' | 'query'> & {
headers: Headers; headers: Headers;
}) => { }) => {
for (const auth of security) { for (const auth of security) {
if (checkForExistence(options, auth.name)) { if (checkForExistence(options, auth.name)) {
continue; continue;
} }
const token = await getAuthToken(auth, options.auth); const token = await getAuthToken(auth, options.auth);
if (!token) { if (!token) {
continue; continue;
} }
const name = auth.name ?? "Authorization"; const name = auth.name ?? 'Authorization';
switch (auth.in) { switch (auth.in) {
case "query": case 'query':
if (!options.query) { if (!options.query) {
options.query = {}; options.query = {};
} }
options.query[name] = token; options.query[name] = token;
break; break;
case "cookie": case 'cookie':
options.headers.append("Cookie", `${name}=${token}`); options.headers.append('Cookie', `${name}=${token}`);
break; break;
case "header": case 'header':
default: default:
options.headers.set(name, token); options.headers.set(name, token);
break; break;
} }
} }
}; };
export const buildUrl: Client["buildUrl"] = (options) => export const buildUrl: Client['buildUrl'] = (options) =>
getUrl({ getUrl({
baseUrl: options.baseUrl as string, baseUrl: options.baseUrl as string,
path: options.path, path: options.path,
query: options.query, query: options.query,
querySerializer: querySerializer:
typeof options.querySerializer === "function" typeof options.querySerializer === 'function'
? options.querySerializer ? options.querySerializer
: createQuerySerializer(options.querySerializer), : createQuerySerializer(options.querySerializer),
url: options.url, url: options.url,
}); });
export const mergeConfigs = (a: Config, b: Config): Config => { export const mergeConfigs = (a: Config, b: Config): Config => {
const config = { ...a, ...b }; const config = { ...a, ...b };
if (config.baseUrl?.endsWith("/")) { if (config.baseUrl?.endsWith('/')) {
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
} }
config.headers = mergeHeaders(a.headers, b.headers); config.headers = mergeHeaders(a.headers, b.headers);
return config; return config;
}; };
const headersEntries = (headers: Headers): Array<[string, string]> => { const headersEntries = (headers: Headers): Array<[string, string]> => {
const entries: Array<[string, string]> = []; const entries: Array<[string, string]> = [];
headers.forEach((value, key) => { headers.forEach((value, key) => {
entries.push([key, value]); entries.push([key, value]);
}); });
return entries; return entries;
}; };
export const mergeHeaders = (...headers: Array<Required<Config>["headers"] | undefined>): Headers => { export const mergeHeaders = (
const mergedHeaders = new Headers(); ...headers: Array<Required<Config>['headers'] | undefined>
for (const header of headers) { ): Headers => {
if (!header) { const mergedHeaders = new Headers();
continue; for (const header of headers) {
} if (!header) {
continue;
}
const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); const iterator =
header instanceof Headers
? headersEntries(header)
: Object.entries(header);
for (const [key, value] of iterator) { for (const [key, value] of iterator) {
if (value === null) { if (value === null) {
mergedHeaders.delete(key); mergedHeaders.delete(key);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
for (const v of value) { for (const v of value) {
mergedHeaders.append(key, v as string); mergedHeaders.append(key, v as string);
} }
} else if (value !== undefined) { } else if (value !== undefined) {
// assume object headers are meant to be JSON stringified, i.e. their // assume object headers are meant to be JSON stringified, i.e. their
// content value in OpenAPI specification is 'application/json' // content value in OpenAPI specification is 'application/json'
mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : (value as string)); mergedHeaders.set(
} key,
} typeof value === 'object' ? JSON.stringify(value) : (value as string),
} );
return mergedHeaders; }
}
}
return mergedHeaders;
}; };
type ErrInterceptor<Err, Res, Req, Options> = ( type ErrInterceptor<Err, Res, Req, Options> = (
error: Err, error: Err,
response: Res, response: Res,
request: Req, request: Req,
options: Options, options: Options,
) => Err | Promise<Err>; ) => Err | Promise<Err>;
type ReqInterceptor<Req, Options> = (request: Req, options: Options) => Req | Promise<Req>; type ReqInterceptor<Req, Options> = (
request: Req,
options: Options,
) => Req | Promise<Req>;
type ResInterceptor<Res, Req, Options> = (response: Res, request: Req, options: Options) => Res | Promise<Res>; type ResInterceptor<Res, Req, Options> = (
response: Res,
request: Req,
options: Options,
) => Res | Promise<Res>;
class Interceptors<Interceptor> { class Interceptors<Interceptor> {
fns: Array<Interceptor | null> = []; fns: Array<Interceptor | null> = [];
clear(): void { clear(): void {
this.fns = []; this.fns = [];
} }
eject(id: number | Interceptor): void { eject(id: number | Interceptor): void {
const index = this.getInterceptorIndex(id); const index = this.getInterceptorIndex(id);
if (this.fns[index]) { if (this.fns[index]) {
this.fns[index] = null; this.fns[index] = null;
} }
} }
exists(id: number | Interceptor): boolean { exists(id: number | Interceptor): boolean {
const index = this.getInterceptorIndex(id); const index = this.getInterceptorIndex(id);
return Boolean(this.fns[index]); return Boolean(this.fns[index]);
} }
getInterceptorIndex(id: number | Interceptor): number { getInterceptorIndex(id: number | Interceptor): number {
if (typeof id === "number") { if (typeof id === 'number') {
return this.fns[id] ? id : -1; return this.fns[id] ? id : -1;
} }
return this.fns.indexOf(id); return this.fns.indexOf(id);
} }
update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { update(
const index = this.getInterceptorIndex(id); id: number | Interceptor,
if (this.fns[index]) { fn: Interceptor,
this.fns[index] = fn; ): number | Interceptor | false {
return id; const index = this.getInterceptorIndex(id);
} if (this.fns[index]) {
return false; this.fns[index] = fn;
} return id;
}
return false;
}
use(fn: Interceptor): number { use(fn: Interceptor): number {
this.fns.push(fn); this.fns.push(fn);
return this.fns.length - 1; return this.fns.length - 1;
} }
} }
export interface Middleware<Req, Res, Err, Options> { export interface Middleware<Req, Res, Err, Options> {
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>; error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
request: Interceptors<ReqInterceptor<Req, Options>>; request: Interceptors<ReqInterceptor<Req, Options>>;
response: Interceptors<ResInterceptor<Res, Req, Options>>; response: Interceptors<ResInterceptor<Res, Req, Options>>;
} }
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<Req, Res, Err, Options> => ({ export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(), Req,
request: new Interceptors<ReqInterceptor<Req, Options>>(), Res,
response: new Interceptors<ResInterceptor<Res, Req, Options>>(), Err,
Options
> => ({
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
request: new Interceptors<ReqInterceptor<Req, Options>>(),
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
}); });
const defaultQuerySerializer = createQuerySerializer({ const defaultQuerySerializer = createQuerySerializer({
allowReserved: false, allowReserved: false,
array: { array: {
explode: true, explode: true,
style: "form", style: 'form',
}, },
object: { object: {
explode: true, explode: true,
style: "deepObject", style: 'deepObject',
}, },
}); });
const defaultHeaders = { const defaultHeaders = {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}; };
export const createConfig = <T extends ClientOptions = ClientOptions>( export const createConfig = <T extends ClientOptions = ClientOptions>(
override: Config<Omit<ClientOptions, keyof T> & T> = {}, override: Config<Omit<ClientOptions, keyof T> & T> = {},
): Config<Omit<ClientOptions, keyof T> & T> => ({ ): Config<Omit<ClientOptions, keyof T> & T> => ({
...jsonBodySerializer, ...jsonBodySerializer,
headers: defaultHeaders, headers: defaultHeaders,
parseAs: "auto", parseAs: 'auto',
querySerializer: defaultQuerySerializer, querySerializer: defaultQuerySerializer,
...override, ...override,
}); });

View File

@@ -3,39 +3,40 @@
export type AuthToken = string | undefined; export type AuthToken = string | undefined;
export interface Auth { export interface Auth {
/** /**
* Which part of the request do we use to send the auth? * Which part of the request do we use to send the auth?
* *
* @default 'header' * @default 'header'
*/ */
in?: "header" | "query" | "cookie"; in?: 'header' | 'query' | 'cookie';
/** /**
* Header or query parameter name. * Header or query parameter name.
* *
* @default 'Authorization' * @default 'Authorization'
*/ */
name?: string; name?: string;
scheme?: "basic" | "bearer"; scheme?: 'basic' | 'bearer';
type: "apiKey" | "http"; type: 'apiKey' | 'http';
} }
export const getAuthToken = async ( export const getAuthToken = async (
auth: Auth, auth: Auth,
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken, callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
): Promise<string | undefined> => { ): Promise<string | undefined> => {
const token = typeof callback === "function" ? await callback(auth) : callback; const token =
typeof callback === 'function' ? await callback(auth) : callback;
if (!token) { if (!token) {
return; return;
} }
if (auth.scheme === "bearer") { if (auth.scheme === 'bearer') {
return `Bearer ${token}`; return `Bearer ${token}`;
} }
if (auth.scheme === "basic") { if (auth.scheme === 'basic') {
return `Basic ${btoa(token)}`; return `Basic ${btoa(token)}`;
} }
return token; return token;
}; };

View File

@@ -1,82 +1,100 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen"; import type {
ArrayStyle,
ObjectStyle,
SerializerOptions,
} from './pathSerializer.gen';
export type QuerySerializer = (query: Record<string, unknown>) => string; export type QuerySerializer = (query: Record<string, unknown>) => string;
export type BodySerializer = (body: any) => any; export type BodySerializer = (body: any) => any;
type QuerySerializerOptionsObject = { type QuerySerializerOptionsObject = {
allowReserved?: boolean; allowReserved?: boolean;
array?: Partial<SerializerOptions<ArrayStyle>>; array?: Partial<SerializerOptions<ArrayStyle>>;
object?: Partial<SerializerOptions<ObjectStyle>>; object?: Partial<SerializerOptions<ObjectStyle>>;
}; };
export type QuerySerializerOptions = QuerySerializerOptionsObject & { export type QuerySerializerOptions = QuerySerializerOptionsObject & {
/** /**
* Per-parameter serialization overrides. When provided, these settings * Per-parameter serialization overrides. When provided, these settings
* override the global array/object settings for specific parameter names. * override the global array/object settings for specific parameter names.
*/ */
parameters?: Record<string, QuerySerializerOptionsObject>; parameters?: Record<string, QuerySerializerOptionsObject>;
}; };
const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { const serializeFormDataPair = (
if (typeof value === "string" || value instanceof Blob) { data: FormData,
data.append(key, value); key: string,
} else if (value instanceof Date) { value: unknown,
data.append(key, value.toISOString()); ): void => {
} else { if (typeof value === 'string' || value instanceof Blob) {
data.append(key, JSON.stringify(value)); data.append(key, value);
} } else if (value instanceof Date) {
data.append(key, value.toISOString());
} else {
data.append(key, JSON.stringify(value));
}
}; };
const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { const serializeUrlSearchParamsPair = (
if (typeof value === "string") { data: URLSearchParams,
data.append(key, value); key: string,
} else { value: unknown,
data.append(key, JSON.stringify(value)); ): void => {
} if (typeof value === 'string') {
data.append(key, value);
} else {
data.append(key, JSON.stringify(value));
}
}; };
export const formDataBodySerializer = { export const formDataBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): FormData => { bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
const data = new FormData(); body: T,
): FormData => {
const data = new FormData();
Object.entries(body).forEach(([key, value]) => { Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) { if (value === undefined || value === null) {
return; return;
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((v) => serializeFormDataPair(data, key, v)); value.forEach((v) => serializeFormDataPair(data, key, v));
} else { } else {
serializeFormDataPair(data, key, value); serializeFormDataPair(data, key, value);
} }
}); });
return data; return data;
}, },
}; };
export const jsonBodySerializer = { export const jsonBodySerializer = {
bodySerializer: <T>(body: T): string => bodySerializer: <T>(body: T): string =>
JSON.stringify(body, (_key, value) => (typeof value === "bigint" ? value.toString() : value)), JSON.stringify(body, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
),
}; };
export const urlSearchParamsBodySerializer = { export const urlSearchParamsBodySerializer = {
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(body: T): string => { bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
const data = new URLSearchParams(); body: T,
): string => {
const data = new URLSearchParams();
Object.entries(body).forEach(([key, value]) => { Object.entries(body).forEach(([key, value]) => {
if (value === undefined || value === null) { if (value === undefined || value === null) {
return; return;
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
} else { } else {
serializeUrlSearchParamsPair(data, key, value); serializeUrlSearchParamsPair(data, key, value);
} }
}); });
return data.toString(); return data.toString();
}, },
}; };

View File

@@ -1,169 +1,176 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
type Slot = "body" | "headers" | "path" | "query"; type Slot = 'body' | 'headers' | 'path' | 'query';
export type Field = export type Field =
| { | {
in: Exclude<Slot, "body">; in: Exclude<Slot, 'body'>;
/** /**
* Field name. This is the name we want the user to see and use. * Field name. This is the name we want the user to see and use.
*/ */
key: string; key: string;
/** /**
* Field mapped name. This is the name we want to use in the request. * Field mapped name. This is the name we want to use in the request.
* If omitted, we use the same value as `key`. * If omitted, we use the same value as `key`.
*/ */
map?: string; map?: string;
} }
| { | {
in: Extract<Slot, "body">; in: Extract<Slot, 'body'>;
/** /**
* Key isn't required for bodies. * Key isn't required for bodies.
*/ */
key?: string; key?: string;
map?: string; map?: string;
} }
| { | {
/** /**
* Field name. This is the name we want the user to see and use. * Field name. This is the name we want the user to see and use.
*/ */
key: string; key: string;
/** /**
* Field mapped name. This is the name we want to use in the request. * Field mapped name. This is the name we want to use in the request.
* If `in` is omitted, `map` aliases `key` to the transport layer. * If `in` is omitted, `map` aliases `key` to the transport layer.
*/ */
map: Slot; map: Slot;
}; };
export interface Fields { export interface Fields {
allowExtra?: Partial<Record<Slot, boolean>>; allowExtra?: Partial<Record<Slot, boolean>>;
args?: ReadonlyArray<Field>; args?: ReadonlyArray<Field>;
} }
export type FieldsConfig = ReadonlyArray<Field | Fields>; export type FieldsConfig = ReadonlyArray<Field | Fields>;
const extraPrefixesMap: Record<string, Slot> = { const extraPrefixesMap: Record<string, Slot> = {
$body_: "body", $body_: 'body',
$headers_: "headers", $headers_: 'headers',
$path_: "path", $path_: 'path',
$query_: "query", $query_: 'query',
}; };
const extraPrefixes = Object.entries(extraPrefixesMap); const extraPrefixes = Object.entries(extraPrefixesMap);
type KeyMap = Map< type KeyMap = Map<
string, string,
| { | {
in: Slot; in: Slot;
map?: string; map?: string;
} }
| { | {
in?: never; in?: never;
map: Slot; map: Slot;
} }
>; >;
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
if (!map) { if (!map) {
map = new Map(); map = new Map();
} }
for (const config of fields) { for (const config of fields) {
if ("in" in config) { if ('in' in config) {
if (config.key) { if (config.key) {
map.set(config.key, { map.set(config.key, {
in: config.in, in: config.in,
map: config.map, map: config.map,
}); });
} }
} else if ("key" in config) { } else if ('key' in config) {
map.set(config.key, { map.set(config.key, {
map: config.map, map: config.map,
}); });
} else if (config.args) { } else if (config.args) {
buildKeyMap(config.args, map); buildKeyMap(config.args, map);
} }
} }
return map; return map;
}; };
interface Params { interface Params {
body: unknown; body: unknown;
headers: Record<string, unknown>; headers: Record<string, unknown>;
path: Record<string, unknown>; path: Record<string, unknown>;
query: Record<string, unknown>; query: Record<string, unknown>;
} }
const stripEmptySlots = (params: Params) => { const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) { for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === "object" && !Object.keys(value).length) { if (value && typeof value === 'object' && !Object.keys(value).length) {
delete params[slot as Slot]; delete params[slot as Slot];
} }
} }
}; };
export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => { export const buildClientParams = (
const params: Params = { args: ReadonlyArray<unknown>,
body: {}, fields: FieldsConfig,
headers: {}, ) => {
path: {}, const params: Params = {
query: {}, body: {},
}; headers: {},
path: {},
query: {},
};
const map = buildKeyMap(fields); const map = buildKeyMap(fields);
let config: FieldsConfig[number] | undefined; let config: FieldsConfig[number] | undefined;
for (const [index, arg] of args.entries()) { for (const [index, arg] of args.entries()) {
if (fields[index]) { if (fields[index]) {
config = fields[index]; config = fields[index];
} }
if (!config) { if (!config) {
continue; continue;
} }
if ("in" in config) { if ('in' in config) {
if (config.key) { if (config.key) {
const field = map.get(config.key)!; const field = map.get(config.key)!;
const name = field.map || config.key; const name = field.map || config.key;
if (field.in) { if (field.in) {
(params[field.in] as Record<string, unknown>)[name] = arg; (params[field.in] as Record<string, unknown>)[name] = arg;
} }
} else { } else {
params.body = arg; params.body = arg;
} }
} else { } else {
for (const [key, value] of Object.entries(arg ?? {})) { for (const [key, value] of Object.entries(arg ?? {})) {
const field = map.get(key); const field = map.get(key);
if (field) { if (field) {
if (field.in) { if (field.in) {
const name = field.map || key; const name = field.map || key;
(params[field.in] as Record<string, unknown>)[name] = value; (params[field.in] as Record<string, unknown>)[name] = value;
} else { } else {
params[field.map] = value; params[field.map] = value;
} }
} else { } else {
const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); const extra = extraPrefixes.find(([prefix]) =>
key.startsWith(prefix),
);
if (extra) { if (extra) {
const [prefix, slot] = extra; const [prefix, slot] = extra;
(params[slot] as Record<string, unknown>)[key.slice(prefix.length)] = value; (params[slot] as Record<string, unknown>)[
} else if ("allowExtra" in config && config.allowExtra) { key.slice(prefix.length)
for (const [slot, allowed] of Object.entries(config.allowExtra)) { ] = value;
if (allowed) { } else if ('allowExtra' in config && config.allowExtra) {
(params[slot as Slot] as Record<string, unknown>)[key] = value; for (const [slot, allowed] of Object.entries(config.allowExtra)) {
break; if (allowed) {
} (params[slot as Slot] as Record<string, unknown>)[key] = value;
} break;
} }
} }
} }
} }
} }
}
}
stripEmptySlots(params); stripEmptySlots(params);
return params; return params;
}; };

View File

@@ -1,167 +1,181 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {} interface SerializeOptions<T>
extends SerializePrimitiveOptions,
SerializerOptions<T> {}
interface SerializePrimitiveOptions { interface SerializePrimitiveOptions {
allowReserved?: boolean; allowReserved?: boolean;
name: string; name: string;
} }
export interface SerializerOptions<T> { export interface SerializerOptions<T> {
/** /**
* @default true * @default true
*/ */
explode: boolean; explode: boolean;
style: T; style: T;
} }
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle = "label" | "matrix" | "simple"; type MatrixStyle = 'label' | 'matrix' | 'simple';
export type ObjectStyle = "form" | "deepObject"; export type ObjectStyle = 'form' | 'deepObject';
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
interface SerializePrimitiveParam extends SerializePrimitiveOptions { interface SerializePrimitiveParam extends SerializePrimitiveOptions {
value: string; value: string;
} }
export const separatorArrayExplode = (style: ArraySeparatorStyle) => { export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
switch (style) { switch (style) {
case "label": case 'label':
return "."; return '.';
case "matrix": case 'matrix':
return ";"; return ';';
case "simple": case 'simple':
return ","; return ',';
default: default:
return "&"; return '&';
} }
}; };
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
switch (style) { switch (style) {
case "form": case 'form':
return ","; return ',';
case "pipeDelimited": case 'pipeDelimited':
return "|"; return '|';
case "spaceDelimited": case 'spaceDelimited':
return "%20"; return '%20';
default: default:
return ","; return ',';
} }
}; };
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
switch (style) { switch (style) {
case "label": case 'label':
return "."; return '.';
case "matrix": case 'matrix':
return ";"; return ';';
case "simple": case 'simple':
return ","; return ',';
default: default:
return "&"; return '&';
} }
}; };
export const serializeArrayParam = ({ export const serializeArrayParam = ({
allowReserved, allowReserved,
explode, explode,
name, name,
style, style,
value, value,
}: SerializeOptions<ArraySeparatorStyle> & { }: SerializeOptions<ArraySeparatorStyle> & {
value: unknown[]; value: unknown[];
}) => { }) => {
if (!explode) { if (!explode) {
const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v as string))).join( const joinedValues = (
separatorArrayNoExplode(style), allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
); ).join(separatorArrayNoExplode(style));
switch (style) { switch (style) {
case "label": case 'label':
return `.${joinedValues}`; return `.${joinedValues}`;
case "matrix": case 'matrix':
return `;${name}=${joinedValues}`; return `;${name}=${joinedValues}`;
case "simple": case 'simple':
return joinedValues; return joinedValues;
default: default:
return `${name}=${joinedValues}`; return `${name}=${joinedValues}`;
} }
} }
const separator = separatorArrayExplode(style); const separator = separatorArrayExplode(style);
const joinedValues = value const joinedValues = value
.map((v) => { .map((v) => {
if (style === "label" || style === "simple") { if (style === 'label' || style === 'simple') {
return allowReserved ? v : encodeURIComponent(v as string); return allowReserved ? v : encodeURIComponent(v as string);
} }
return serializePrimitiveParam({ return serializePrimitiveParam({
allowReserved, allowReserved,
name, name,
value: v as string, value: v as string,
}); });
}) })
.join(separator); .join(separator);
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
}; };
export const serializePrimitiveParam = ({ allowReserved, name, value }: SerializePrimitiveParam) => { export const serializePrimitiveParam = ({
if (value === undefined || value === null) { allowReserved,
return ""; name,
} value,
}: SerializePrimitiveParam) => {
if (value === undefined || value === null) {
return '';
}
if (typeof value === "object") { if (typeof value === 'object') {
throw new Error( throw new Error(
"Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.", 'Deeply-nested arrays/objects arent supported. Provide your own `querySerializer()` to handle these.',
); );
} }
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
}; };
export const serializeObjectParam = ({ export const serializeObjectParam = ({
allowReserved, allowReserved,
explode, explode,
name, name,
style, style,
value, value,
valueOnly, valueOnly,
}: SerializeOptions<ObjectSeparatorStyle> & { }: SerializeOptions<ObjectSeparatorStyle> & {
value: Record<string, unknown> | Date; value: Record<string, unknown> | Date;
valueOnly?: boolean; valueOnly?: boolean;
}) => { }) => {
if (value instanceof Date) { if (value instanceof Date) {
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
} }
if (style !== "deepObject" && !explode) { if (style !== 'deepObject' && !explode) {
let values: string[] = []; let values: string[] = [];
Object.entries(value).forEach(([key, v]) => { Object.entries(value).forEach(([key, v]) => {
values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; values = [
}); ...values,
const joinedValues = values.join(","); key,
switch (style) { allowReserved ? (v as string) : encodeURIComponent(v as string),
case "form": ];
return `${name}=${joinedValues}`; });
case "label": const joinedValues = values.join(',');
return `.${joinedValues}`; switch (style) {
case "matrix": case 'form':
return `;${name}=${joinedValues}`; return `${name}=${joinedValues}`;
default: case 'label':
return joinedValues; return `.${joinedValues}`;
} case 'matrix':
} return `;${name}=${joinedValues}`;
default:
return joinedValues;
}
}
const separator = separatorObjectExplode(style); const separator = separatorObjectExplode(style);
const joinedValues = Object.entries(value) const joinedValues = Object.entries(value)
.map(([key, v]) => .map(([key, v]) =>
serializePrimitiveParam({ serializePrimitiveParam({
allowReserved, allowReserved,
name: style === "deepObject" ? `${name}[${key}]` : key, name: style === 'deepObject' ? `${name}[${key}]` : key,
value: v as string, value: v as string,
}), }),
) )
.join(separator); .join(separator);
return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues; return style === 'label' || style === 'matrix'
? separator + joinedValues
: joinedValues;
}; };

View File

@@ -3,109 +3,134 @@
/** /**
* JSON-friendly union that mirrors what Pinia Colada can hash. * JSON-friendly union that mirrors what Pinia Colada can hash.
*/ */
export type JsonValue = null | string | number | boolean | JsonValue[] | { [key: string]: JsonValue }; export type JsonValue =
| null
| string
| number
| boolean
| JsonValue[]
| { [key: string]: JsonValue };
/** /**
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
*/ */
export const queryKeyJsonReplacer = (_key: string, value: unknown) => { export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
if (value === undefined || typeof value === "function" || typeof value === "symbol") { if (
return undefined; value === undefined ||
} typeof value === 'function' ||
if (typeof value === "bigint") { typeof value === 'symbol'
return value.toString(); ) {
} return undefined;
if (value instanceof Date) { }
return value.toISOString(); if (typeof value === 'bigint') {
} return value.toString();
return value; }
if (value instanceof Date) {
return value.toISOString();
}
return value;
}; };
/** /**
* Safely stringifies a value and parses it back into a JsonValue. * Safely stringifies a value and parses it back into a JsonValue.
*/ */
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
try { try {
const json = JSON.stringify(input, queryKeyJsonReplacer); const json = JSON.stringify(input, queryKeyJsonReplacer);
if (json === undefined) { if (json === undefined) {
return undefined; return undefined;
} }
return JSON.parse(json) as JsonValue; return JSON.parse(json) as JsonValue;
} catch { } catch {
return undefined; return undefined;
} }
}; };
/** /**
* Detects plain objects (including objects with a null prototype). * Detects plain objects (including objects with a null prototype).
*/ */
const isPlainObject = (value: unknown): value is Record<string, unknown> => { const isPlainObject = (value: unknown): value is Record<string, unknown> => {
if (value === null || typeof value !== "object") { if (value === null || typeof value !== 'object') {
return false; return false;
} }
const prototype = Object.getPrototypeOf(value as object); const prototype = Object.getPrototypeOf(value as object);
return prototype === Object.prototype || prototype === null; return prototype === Object.prototype || prototype === null;
}; };
/** /**
* Turns URLSearchParams into a sorted JSON object for deterministic keys. * Turns URLSearchParams into a sorted JSON object for deterministic keys.
*/ */
const serializeSearchParams = (params: URLSearchParams): JsonValue => { const serializeSearchParams = (params: URLSearchParams): JsonValue => {
const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); const entries = Array.from(params.entries()).sort(([a], [b]) =>
const result: Record<string, JsonValue> = {}; a.localeCompare(b),
);
const result: Record<string, JsonValue> = {};
for (const [key, value] of entries) { for (const [key, value] of entries) {
const existing = result[key]; const existing = result[key];
if (existing === undefined) { if (existing === undefined) {
result[key] = value; result[key] = value;
continue; continue;
} }
if (Array.isArray(existing)) { if (Array.isArray(existing)) {
(existing as string[]).push(value); (existing as string[]).push(value);
} else { } else {
result[key] = [existing, value]; result[key] = [existing, value];
} }
} }
return result; return result;
}; };
/** /**
* Normalizes any accepted value into a JSON-friendly shape for query keys. * Normalizes any accepted value into a JSON-friendly shape for query keys.
*/ */
export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { export const serializeQueryKeyValue = (
if (value === null) { value: unknown,
return null; ): JsonValue | undefined => {
} if (value === null) {
return null;
}
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { if (
return value; typeof value === 'string' ||
} typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
}
if (value === undefined || typeof value === "function" || typeof value === "symbol") { if (
return undefined; value === undefined ||
} typeof value === 'function' ||
typeof value === 'symbol'
) {
return undefined;
}
if (typeof value === "bigint") { if (typeof value === 'bigint') {
return value.toString(); return value.toString();
} }
if (value instanceof Date) { if (value instanceof Date) {
return value.toISOString(); return value.toISOString();
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
return stringifyToJsonValue(value); return stringifyToJsonValue(value);
} }
if (typeof URLSearchParams !== "undefined" && value instanceof URLSearchParams) { if (
return serializeSearchParams(value); typeof URLSearchParams !== 'undefined' &&
} value instanceof URLSearchParams
) {
return serializeSearchParams(value);
}
if (isPlainObject(value)) { if (isPlainObject(value)) {
return stringifyToJsonValue(value); return stringifyToJsonValue(value);
} }
return undefined; return undefined;
}; };

View File

@@ -1,237 +1,264 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Config } from "./types.gen"; import type { Config } from './types.gen';
export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> & export type ServerSentEventsOptions<TData = unknown> = Omit<
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & { RequestInit,
/** 'method'
* Fetch API implementation. You can use this option to provide a custom > &
* fetch instance. Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
* /**
* @default globalThis.fetch * Fetch API implementation. You can use this option to provide a custom
*/ * fetch instance.
fetch?: typeof fetch; *
/** * @default globalThis.fetch
* Implementing clients can call request interceptors inside this hook. */
*/ fetch?: typeof fetch;
onRequest?: (url: string, init: RequestInit) => Promise<Request>; /**
/** * Implementing clients can call request interceptors inside this hook.
* Callback invoked when a network or parsing error occurs during streaming. */
* onRequest?: (url: string, init: RequestInit) => Promise<Request>;
* This option applies only if the endpoint returns a stream of events. /**
* * Callback invoked when a network or parsing error occurs during streaming.
* @param error The error that occurred. *
*/ * This option applies only if the endpoint returns a stream of events.
onSseError?: (error: unknown) => void; *
/** * @param error The error that occurred.
* Callback invoked when an event is streamed from the server. */
* onSseError?: (error: unknown) => void;
* This option applies only if the endpoint returns a stream of events. /**
* * Callback invoked when an event is streamed from the server.
* @param event Event streamed from the server. *
* @returns Nothing (void). * This option applies only if the endpoint returns a stream of events.
*/ *
onSseEvent?: (event: StreamEvent<TData>) => void; * @param event Event streamed from the server.
serializedBody?: RequestInit["body"]; * @returns Nothing (void).
/** */
* Default retry delay in milliseconds. onSseEvent?: (event: StreamEvent<TData>) => void;
* serializedBody?: RequestInit['body'];
* This option applies only if the endpoint returns a stream of events. /**
* * Default retry delay in milliseconds.
* @default 3000 *
*/ * This option applies only if the endpoint returns a stream of events.
sseDefaultRetryDelay?: number; *
/** * @default 3000
* Maximum number of retry attempts before giving up. */
*/ sseDefaultRetryDelay?: number;
sseMaxRetryAttempts?: number; /**
/** * Maximum number of retry attempts before giving up.
* Maximum retry delay in milliseconds. */
* sseMaxRetryAttempts?: number;
* Applies only when exponential backoff is used. /**
* * Maximum retry delay in milliseconds.
* This option applies only if the endpoint returns a stream of events. *
* * Applies only when exponential backoff is used.
* @default 30000 *
*/ * This option applies only if the endpoint returns a stream of events.
sseMaxRetryDelay?: number; *
/** * @default 30000
* Optional sleep function for retry backoff. */
* sseMaxRetryDelay?: number;
* Defaults to using `setTimeout`. /**
*/ * Optional sleep function for retry backoff.
sseSleepFn?: (ms: number) => Promise<void>; *
url: string; * Defaults to using `setTimeout`.
}; */
sseSleepFn?: (ms: number) => Promise<void>;
url: string;
};
export interface StreamEvent<TData = unknown> { export interface StreamEvent<TData = unknown> {
data: TData; data: TData;
event?: string; event?: string;
id?: string; id?: string;
retry?: number; retry?: number;
} }
export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = { export type ServerSentEventsResult<
stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>; TData = unknown,
TReturn = void,
TNext = unknown,
> = {
stream: AsyncGenerator<
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
TReturn,
TNext
>;
}; };
export const createSseClient = <TData = unknown>({ export const createSseClient = <TData = unknown>({
onRequest, onRequest,
onSseError, onSseError,
onSseEvent, onSseEvent,
responseTransformer, responseTransformer,
responseValidator, responseValidator,
sseDefaultRetryDelay, sseDefaultRetryDelay,
sseMaxRetryAttempts, sseMaxRetryAttempts,
sseMaxRetryDelay, sseMaxRetryDelay,
sseSleepFn, sseSleepFn,
url, url,
...options ...options
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => { }: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
let lastEventId: string | undefined; let lastEventId: string | undefined;
const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); const sleep =
sseSleepFn ??
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
const createStream = async function* () { const createStream = async function* () {
let retryDelay: number = sseDefaultRetryDelay ?? 3000; let retryDelay: number = sseDefaultRetryDelay ?? 3000;
let attempt = 0; let attempt = 0;
const signal = options.signal ?? new AbortController().signal; const signal = options.signal ?? new AbortController().signal;
while (true) { while (true) {
if (signal.aborted) break; if (signal.aborted) break;
attempt++; attempt++;
const headers = const headers =
options.headers instanceof Headers options.headers instanceof Headers
? options.headers ? options.headers
: new Headers(options.headers as Record<string, string> | undefined); : new Headers(options.headers as Record<string, string> | undefined);
if (lastEventId !== undefined) { if (lastEventId !== undefined) {
headers.set("Last-Event-ID", lastEventId); headers.set('Last-Event-ID', lastEventId);
} }
try { try {
const requestInit: RequestInit = { const requestInit: RequestInit = {
redirect: "follow", redirect: 'follow',
...options, ...options,
body: options.serializedBody, body: options.serializedBody,
headers, headers,
signal, signal,
}; };
let request = new Request(url, requestInit); let request = new Request(url, requestInit);
if (onRequest) { if (onRequest) {
request = await onRequest(url, requestInit); request = await onRequest(url, requestInit);
} }
// fetch must be assigned here, otherwise it would throw the error: // fetch must be assigned here, otherwise it would throw the error:
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
const _fetch = options.fetch ?? globalThis.fetch; const _fetch = options.fetch ?? globalThis.fetch;
const response = await _fetch(request); const response = await _fetch(request);
if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); if (!response.ok)
throw new Error(
`SSE failed: ${response.status} ${response.statusText}`,
);
if (!response.body) throw new Error("No body in SSE response"); if (!response.body) throw new Error('No body in SSE response');
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); const reader = response.body
.pipeThrough(new TextDecoderStream())
.getReader();
let buffer = ""; let buffer = '';
const abortHandler = () => { const abortHandler = () => {
try { try {
reader.cancel(); reader.cancel();
} catch { } catch {
// noop // noop
} }
}; };
signal.addEventListener("abort", abortHandler); signal.addEventListener('abort', abortHandler);
try { try {
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) break;
buffer += value; buffer += value;
const chunks = buffer.split("\n\n"); const chunks = buffer.split('\n\n');
buffer = chunks.pop() ?? ""; buffer = chunks.pop() ?? '';
for (const chunk of chunks) { for (const chunk of chunks) {
const lines = chunk.split("\n"); const lines = chunk.split('\n');
const dataLines: Array<string> = []; const dataLines: Array<string> = [];
let eventName: string | undefined; let eventName: string | undefined;
for (const line of lines) { for (const line of lines) {
if (line.startsWith("data:")) { if (line.startsWith('data:')) {
dataLines.push(line.replace(/^data:\s*/, "")); dataLines.push(line.replace(/^data:\s*/, ''));
} else if (line.startsWith("event:")) { } else if (line.startsWith('event:')) {
eventName = line.replace(/^event:\s*/, ""); eventName = line.replace(/^event:\s*/, '');
} else if (line.startsWith("id:")) { } else if (line.startsWith('id:')) {
lastEventId = line.replace(/^id:\s*/, ""); lastEventId = line.replace(/^id:\s*/, '');
} else if (line.startsWith("retry:")) { } else if (line.startsWith('retry:')) {
const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10); const parsed = Number.parseInt(
if (!Number.isNaN(parsed)) { line.replace(/^retry:\s*/, ''),
retryDelay = parsed; 10,
} );
} if (!Number.isNaN(parsed)) {
} retryDelay = parsed;
}
}
}
let data: unknown; let data: unknown;
let parsedJson = false; let parsedJson = false;
if (dataLines.length) { if (dataLines.length) {
const rawData = dataLines.join("\n"); const rawData = dataLines.join('\n');
try { try {
data = JSON.parse(rawData); data = JSON.parse(rawData);
parsedJson = true; parsedJson = true;
} catch { } catch {
data = rawData; data = rawData;
} }
} }
if (parsedJson) { if (parsedJson) {
if (responseValidator) { if (responseValidator) {
await responseValidator(data); await responseValidator(data);
} }
if (responseTransformer) { if (responseTransformer) {
data = await responseTransformer(data); data = await responseTransformer(data);
} }
} }
onSseEvent?.({ onSseEvent?.({
data, data,
event: eventName, event: eventName,
id: lastEventId, id: lastEventId,
retry: retryDelay, retry: retryDelay,
}); });
if (dataLines.length) { if (dataLines.length) {
yield data as any; yield data as any;
} }
} }
} }
} finally { } finally {
signal.removeEventListener("abort", abortHandler); signal.removeEventListener('abort', abortHandler);
reader.releaseLock(); reader.releaseLock();
} }
break; // exit loop on normal completion break; // exit loop on normal completion
} catch (error) { } catch (error) {
// connection failed or aborted; retry after delay // connection failed or aborted; retry after delay
onSseError?.(error); onSseError?.(error);
if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { if (
break; // stop after firing error sseMaxRetryAttempts !== undefined &&
} attempt >= sseMaxRetryAttempts
) {
break; // stop after firing error
}
// exponential backoff: double retry each attempt, cap at 30s // exponential backoff: double retry each attempt, cap at 30s
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); const backoff = Math.min(
await sleep(backoff); retryDelay * 2 ** (attempt - 1),
} sseMaxRetryDelay ?? 30000,
} );
}; await sleep(backoff);
}
}
};
const stream = createStream(); const stream = createStream();
return { stream }; return { stream };
}; };

View File

@@ -1,86 +1,118 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Auth, AuthToken } from "./auth.gen"; import type { Auth, AuthToken } from './auth.gen';
import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen"; import type {
BodySerializer,
QuerySerializer,
QuerySerializerOptions,
} from './bodySerializer.gen';
export type HttpMethod = "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace"; export type HttpMethod =
| 'connect'
| 'delete'
| 'get'
| 'head'
| 'options'
| 'patch'
| 'post'
| 'put'
| 'trace';
export type Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, SseFn = never> = { export type Client<
/** RequestFn = never,
* Returns the final request URL. Config = unknown,
*/ MethodFn = never,
buildUrl: BuildUrlFn; BuildUrlFn = never,
getConfig: () => Config; SseFn = never,
request: RequestFn; > = {
setConfig: (config: Config) => Config; /**
* Returns the final request URL.
*/
buildUrl: BuildUrlFn;
getConfig: () => Config;
request: RequestFn;
setConfig: (config: Config) => Config;
} & { } & {
[K in HttpMethod]: MethodFn; [K in HttpMethod]: MethodFn;
} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); } & ([SseFn] extends [never]
? { sse?: never }
: { sse: { [K in HttpMethod]: SseFn } });
export interface Config { export interface Config {
/** /**
* Auth token or a function returning auth token. The resolved value will be * Auth token or a function returning auth token. The resolved value will be
* added to the request payload as defined by its `security` array. * added to the request payload as defined by its `security` array.
*/ */
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken; auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
/** /**
* A function for serializing request body parameter. By default, * A function for serializing request body parameter. By default,
* {@link JSON.stringify()} will be used. * {@link JSON.stringify()} will be used.
*/ */
bodySerializer?: BodySerializer | null; bodySerializer?: BodySerializer | null;
/** /**
* An object containing any HTTP headers that you want to pre-populate your * An object containing any HTTP headers that you want to pre-populate your
* `Headers` object with. * `Headers` object with.
* *
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
*/ */
headers?: headers?:
| RequestInit["headers"] | RequestInit['headers']
| Record<string, string | number | boolean | (string | number | boolean)[] | null | undefined | unknown>; | Record<
/** string,
* The request method. | string
* | number
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} | boolean
*/ | (string | number | boolean)[]
method?: Uppercase<HttpMethod>; | null
/** | undefined
* A function for serializing request query parameters. By default, arrays | unknown
* will be exploded in form style, objects will be exploded in deepObject >;
* style, and reserved characters are percent-encoded. /**
* * The request method.
* This method will have no effect if the native `paramsSerializer()` Axios *
* API function is used. * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
* */
* {@link https://swagger.io/docs/specification/serialization/#query View examples} method?: Uppercase<HttpMethod>;
*/ /**
querySerializer?: QuerySerializer | QuerySerializerOptions; * A function for serializing request query parameters. By default, arrays
/** * will be exploded in form style, objects will be exploded in deepObject
* A function validating request data. This is useful if you want to ensure * style, and reserved characters are percent-encoded.
* the request conforms to the desired shape, so it can be safely sent to *
* the server. * This method will have no effect if the native `paramsSerializer()` Axios
*/ * API function is used.
requestValidator?: (data: unknown) => Promise<unknown>; *
/** * {@link https://swagger.io/docs/specification/serialization/#query View examples}
* A function transforming response data before it's returned. This is useful */
* for post-processing data, e.g. converting ISO strings into Date objects. querySerializer?: QuerySerializer | QuerySerializerOptions;
*/ /**
responseTransformer?: (data: unknown) => Promise<unknown>; * A function validating request data. This is useful if you want to ensure
/** * the request conforms to the desired shape, so it can be safely sent to
* A function validating response data. This is useful if you want to ensure * the server.
* the response conforms to the desired shape, so it can be safely passed to */
* the transformers and returned to the user. requestValidator?: (data: unknown) => Promise<unknown>;
*/ /**
responseValidator?: (data: unknown) => Promise<unknown>; * A function transforming response data before it's returned. This is useful
* for post-processing data, e.g. converting ISO strings into Date objects.
*/
responseTransformer?: (data: unknown) => Promise<unknown>;
/**
* A function validating response data. This is useful if you want to ensure
* the response conforms to the desired shape, so it can be safely passed to
* the transformers and returned to the user.
*/
responseValidator?: (data: unknown) => Promise<unknown>;
} }
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never] type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
? true ? true
: [T] extends [never | undefined] : [T] extends [never | undefined]
? [undefined] extends [T] ? [undefined] extends [T]
? false ? false
: true : true
: false; : false;
export type OmitNever<T extends Record<string, unknown>> = { export type OmitNever<T extends Record<string, unknown>> = {
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true ? never : K]: T[K]; [K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
? never
: K]: T[K];
}; };

View File

@@ -1,137 +1,143 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen"; import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
import { import {
type ArraySeparatorStyle, type ArraySeparatorStyle,
serializeArrayParam, serializeArrayParam,
serializeObjectParam, serializeObjectParam,
serializePrimitiveParam, serializePrimitiveParam,
} from "./pathSerializer.gen"; } from './pathSerializer.gen';
export interface PathSerializer { export interface PathSerializer {
path: Record<string, unknown>; path: Record<string, unknown>;
url: string; url: string;
} }
export const PATH_PARAM_RE = /\{[^{}]+\}/g; export const PATH_PARAM_RE = /\{[^{}]+\}/g;
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
let url = _url; let url = _url;
const matches = _url.match(PATH_PARAM_RE); const matches = _url.match(PATH_PARAM_RE);
if (matches) { if (matches) {
for (const match of matches) { for (const match of matches) {
let explode = false; let explode = false;
let name = match.substring(1, match.length - 1); let name = match.substring(1, match.length - 1);
let style: ArraySeparatorStyle = "simple"; let style: ArraySeparatorStyle = 'simple';
if (name.endsWith("*")) { if (name.endsWith('*')) {
explode = true; explode = true;
name = name.substring(0, name.length - 1); name = name.substring(0, name.length - 1);
} }
if (name.startsWith(".")) { if (name.startsWith('.')) {
name = name.substring(1); name = name.substring(1);
style = "label"; style = 'label';
} else if (name.startsWith(";")) { } else if (name.startsWith(';')) {
name = name.substring(1); name = name.substring(1);
style = "matrix"; style = 'matrix';
} }
const value = path[name]; const value = path[name];
if (value === undefined || value === null) { if (value === undefined || value === null) {
continue; continue;
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
url = url.replace(match, serializeArrayParam({ explode, name, style, value })); url = url.replace(
continue; match,
} serializeArrayParam({ explode, name, style, value }),
);
continue;
}
if (typeof value === "object") { if (typeof value === 'object') {
url = url.replace( url = url.replace(
match, match,
serializeObjectParam({ serializeObjectParam({
explode, explode,
name, name,
style, style,
value: value as Record<string, unknown>, value: value as Record<string, unknown>,
valueOnly: true, valueOnly: true,
}), }),
); );
continue; continue;
} }
if (style === "matrix") { if (style === 'matrix') {
url = url.replace( url = url.replace(
match, match,
`;${serializePrimitiveParam({ `;${serializePrimitiveParam({
name, name,
value: value as string, value: value as string,
})}`, })}`,
); );
continue; continue;
} }
const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string)); const replaceValue = encodeURIComponent(
url = url.replace(match, replaceValue); style === 'label' ? `.${value as string}` : (value as string),
} );
} url = url.replace(match, replaceValue);
return url; }
}
return url;
}; };
export const getUrl = ({ export const getUrl = ({
baseUrl, baseUrl,
path, path,
query, query,
querySerializer, querySerializer,
url: _url, url: _url,
}: { }: {
baseUrl?: string; baseUrl?: string;
path?: Record<string, unknown>; path?: Record<string, unknown>;
query?: Record<string, unknown>; query?: Record<string, unknown>;
querySerializer: QuerySerializer; querySerializer: QuerySerializer;
url: string; url: string;
}) => { }) => {
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
let url = (baseUrl ?? "") + pathUrl; let url = (baseUrl ?? '') + pathUrl;
if (path) { if (path) {
url = defaultPathSerializer({ path, url }); url = defaultPathSerializer({ path, url });
} }
let search = query ? querySerializer(query) : ""; let search = query ? querySerializer(query) : '';
if (search.startsWith("?")) { if (search.startsWith('?')) {
search = search.substring(1); search = search.substring(1);
} }
if (search) { if (search) {
url += `?${search}`; url += `?${search}`;
} }
return url; return url;
}; };
export function getValidRequestBody(options: { export function getValidRequestBody(options: {
body?: unknown; body?: unknown;
bodySerializer?: BodySerializer | null; bodySerializer?: BodySerializer | null;
serializedBody?: unknown; serializedBody?: unknown;
}) { }) {
const hasBody = options.body !== undefined; const hasBody = options.body !== undefined;
const isSerializedBody = hasBody && options.bodySerializer; const isSerializedBody = hasBody && options.bodySerializer;
if (isSerializedBody) { if (isSerializedBody) {
if ("serializedBody" in options) { if ('serializedBody' in options) {
const hasSerializedBody = options.serializedBody !== undefined && options.serializedBody !== ""; const hasSerializedBody =
options.serializedBody !== undefined && options.serializedBody !== '';
return hasSerializedBody ? options.serializedBody : null; return hasSerializedBody ? options.serializedBody : null;
} }
// not all clients implement a serializedBody property (i.e. client-axios) // not all clients implement a serializedBody property (i.e. client-axios)
return options.body !== "" ? options.body : null; return options.body !== '' ? options.body : null;
} }
// plain/text body // plain/text body
if (hasBody) { if (hasBody) {
return options.body; return options.body;
} }
// no body was provided // no body was provided
return undefined; return undefined;
} }

View File

@@ -1,4 +1,4 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
export type * from "./types.gen"; export type * from './types.gen';
export * from "./sdk.gen"; export * from './sdk.gen';

View File

@@ -1,588 +1,453 @@
// This file is auto-generated by @hey-api/openapi-ts // This file is auto-generated by @hey-api/openapi-ts
import type { Client, Options as Options2, TDataShape } from "./client"; import type { Client, Options as Options2, TDataShape } from './client';
import { client } from "./client.gen"; import { client } from './client.gen';
import type { import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetRepositoryData, GetRepositoryResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetErrors, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
BrowseFilesystemData,
BrowseFilesystemResponses,
ChangePasswordData,
ChangePasswordResponses,
CreateBackupScheduleData,
CreateBackupScheduleResponses,
CreateRepositoryData,
CreateRepositoryResponses,
CreateVolumeData,
CreateVolumeResponses,
DeleteBackupScheduleData,
DeleteBackupScheduleResponses,
DeleteRepositoryData,
DeleteRepositoryResponses,
DeleteVolumeData,
DeleteVolumeResponses,
DoctorRepositoryData,
DoctorRepositoryResponses,
DownloadResticPasswordData,
DownloadResticPasswordResponses,
GetBackupScheduleData,
GetBackupScheduleForVolumeData,
GetBackupScheduleForVolumeResponses,
GetBackupScheduleResponses,
GetContainersUsingVolumeData,
GetContainersUsingVolumeErrors,
GetContainersUsingVolumeResponses,
GetMeData,
GetMeResponses,
GetRepositoryData,
GetRepositoryResponses,
GetSnapshotDetailsData,
GetSnapshotDetailsResponses,
GetStatusData,
GetStatusResponses,
GetSystemInfoData,
GetSystemInfoResponses,
GetVolumeData,
GetVolumeErrors,
GetVolumeResponses,
HealthCheckVolumeData,
HealthCheckVolumeErrors,
HealthCheckVolumeResponses,
ListBackupSchedulesData,
ListBackupSchedulesResponses,
ListFilesData,
ListFilesResponses,
ListRcloneRemotesData,
ListRcloneRemotesResponses,
ListRepositoriesData,
ListRepositoriesResponses,
ListSnapshotFilesData,
ListSnapshotFilesResponses,
ListSnapshotsData,
ListSnapshotsResponses,
ListVolumesData,
ListVolumesResponses,
LoginData,
LoginResponses,
LogoutData,
LogoutResponses,
MountVolumeData,
MountVolumeResponses,
RegisterData,
RegisterResponses,
RestoreSnapshotData,
RestoreSnapshotResponses,
RunBackupNowData,
RunBackupNowResponses,
StopBackupData,
StopBackupErrors,
StopBackupResponses,
TestConnectionData,
TestConnectionResponses,
UnmountVolumeData,
UnmountVolumeResponses,
UpdateBackupScheduleData,
UpdateBackupScheduleResponses,
UpdateVolumeData,
UpdateVolumeErrors,
UpdateVolumeResponses,
} from "./types.gen";
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2< export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
TData, /**
ThrowOnError * You can provide a client instance returned by `createClient()` instead of
> & { * individual options. This might be also useful if you want to implement a
/** * custom client.
* You can provide a client instance returned by `createClient()` instead of */
* individual options. This might be also useful if you want to implement a client?: Client;
* custom client. /**
*/ * You can pass arbitrary values through the `meta` object. This can be
client?: Client; * used to access values that aren't defined as part of the SDK function.
/** */
* You can pass arbitrary values through the `meta` object. This can be meta?: Record<string, unknown>;
* used to access values that aren't defined as part of the SDK function.
*/
meta?: Record<string, unknown>;
}; };
/** /**
* Register a new user * Register a new user
*/ */
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => { export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
return (options?.client ?? client).post<RegisterResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<RegisterResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/register", url: '/api/v1/auth/register',
...options, ...options,
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
...options?.headers, ...options?.headers
}, }
}); });
}; };
/** /**
* Login with username and password * Login with username and password
*/ */
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => { export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
return (options?.client ?? client).post<LoginResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<LoginResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/login", url: '/api/v1/auth/login',
...options, ...options,
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
...options?.headers, ...options?.headers
}, }
}); });
}; };
/** /**
* Logout current user * Logout current user
*/ */
export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => { export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => {
return (options?.client ?? client).post<LogoutResponses, unknown, ThrowOnError>({ return (options?.client ?? client).post<LogoutResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/logout", url: '/api/v1/auth/logout',
...options, ...options
}); });
}; };
/** /**
* Get current authenticated user * Get current authenticated user
*/ */
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => { export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
return (options?.client ?? client).get<GetMeResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<GetMeResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/me", url: '/api/v1/auth/me',
...options, ...options
}); });
}; };
/** /**
* Get authentication system status * Get authentication system status
*/ */
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => { export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => {
return (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({
url: "/api/v1/auth/status", url: '/api/v1/auth/status',
...options, ...options
}); });
}; };
/** /**
* Change current user password * Change current user password
*/ */
export const changePassword = <ThrowOnError extends boolean = false>( export const changePassword = <ThrowOnError extends boolean = false>(options?: Options<ChangePasswordData, ThrowOnError>) => {
options?: Options<ChangePasswordData, ThrowOnError>, return (options?.client ?? client).post<ChangePasswordResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/auth/change-password',
return (options?.client ?? client).post<ChangePasswordResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/auth/change-password", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options?.headers
"Content-Type": "application/json", }
...options?.headers, });
},
});
}; };
/** /**
* List all volumes * List all volumes
*/ */
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => { export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => {
return (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({ return (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes", url: '/api/v1/volumes',
...options, ...options
}); });
}; };
/** /**
* Create a new volume * Create a new volume
*/ */
export const createVolume = <ThrowOnError extends boolean = false>( export const createVolume = <ThrowOnError extends boolean = false>(options?: Options<CreateVolumeData, ThrowOnError>) => {
options?: Options<CreateVolumeData, ThrowOnError>, return (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/volumes',
return (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/volumes", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options?.headers
"Content-Type": "application/json", }
...options?.headers, });
},
});
}; };
/** /**
* Test connection to backend * Test connection to backend
*/ */
export const testConnection = <ThrowOnError extends boolean = false>( export const testConnection = <ThrowOnError extends boolean = false>(options?: Options<TestConnectionData, ThrowOnError>) => {
options?: Options<TestConnectionData, ThrowOnError>, return (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/volumes/test-connection',
return (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/volumes/test-connection", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options?.headers
"Content-Type": "application/json", }
...options?.headers, });
},
});
}; };
/** /**
* Delete a volume * Delete a volume
*/ */
export const deleteVolume = <ThrowOnError extends boolean = false>( export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>) => {
options: Options<DeleteVolumeData, ThrowOnError>, return (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/volumes/{name}',
return (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/volumes/{name}", });
...options,
});
}; };
/** /**
* Get a volume by name * Get a volume by name
*/ */
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => { export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => {
return (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({ return (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({
url: "/api/v1/volumes/{name}", url: '/api/v1/volumes/{name}',
...options, ...options
}); });
}; };
/** /**
* Update a volume's configuration * Update a volume's configuration
*/ */
export const updateVolume = <ThrowOnError extends boolean = false>( export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>) => {
options: Options<UpdateVolumeData, ThrowOnError>, return (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
) => { url: '/api/v1/volumes/{name}',
return (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({ ...options,
url: "/api/v1/volumes/{name}", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options.headers
"Content-Type": "application/json", }
...options.headers, });
},
});
}; };
/** /**
* Get containers using a volume by name * Get containers using a volume by name
*/ */
export const getContainersUsingVolume = <ThrowOnError extends boolean = false>( export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(options: Options<GetContainersUsingVolumeData, ThrowOnError>) => {
options: Options<GetContainersUsingVolumeData, ThrowOnError>, return (options.client ?? client).get<GetContainersUsingVolumeResponses, GetContainersUsingVolumeErrors, ThrowOnError>({
) => { url: '/api/v1/volumes/{name}/containers',
return (options.client ?? client).get< ...options
GetContainersUsingVolumeResponses, });
GetContainersUsingVolumeErrors,
ThrowOnError
>({
url: "/api/v1/volumes/{name}/containers",
...options,
});
}; };
/** /**
* Mount a volume * Mount a volume
*/ */
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => { export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
return (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({ return (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/mount", url: '/api/v1/volumes/{name}/mount',
...options, ...options
}); });
}; };
/** /**
* Unmount a volume * Unmount a volume
*/ */
export const unmountVolume = <ThrowOnError extends boolean = false>( export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>) => {
options: Options<UnmountVolumeData, ThrowOnError>, return (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/volumes/{name}/unmount',
return (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/volumes/{name}/unmount", });
...options,
});
}; };
/** /**
* Perform a health check on a volume * Perform a health check on a volume
*/ */
export const healthCheckVolume = <ThrowOnError extends boolean = false>( export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>) => {
options: Options<HealthCheckVolumeData, ThrowOnError>, return (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({
) => { url: '/api/v1/volumes/{name}/health-check',
return (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({ ...options
url: "/api/v1/volumes/{name}/health-check", });
...options,
});
}; };
/** /**
* List files in a volume directory * List files in a volume directory
*/ */
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => { export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
return (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({ return (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({
url: "/api/v1/volumes/{name}/files", url: '/api/v1/volumes/{name}/files',
...options, ...options
}); });
}; };
/** /**
* Browse directories on the host filesystem * Browse directories on the host filesystem
*/ */
export const browseFilesystem = <ThrowOnError extends boolean = false>( export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>) => {
options?: Options<BrowseFilesystemData, ThrowOnError>, return (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/volumes/filesystem/browse',
return (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/volumes/filesystem/browse", });
...options,
});
}; };
/** /**
* List all repositories * List all repositories
*/ */
export const listRepositories = <ThrowOnError extends boolean = false>( export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>) => {
options?: Options<ListRepositoriesData, ThrowOnError>, return (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories',
return (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories", });
...options,
});
}; };
/** /**
* Create a new restic repository * Create a new restic repository
*/ */
export const createRepository = <ThrowOnError extends boolean = false>( export const createRepository = <ThrowOnError extends boolean = false>(options?: Options<CreateRepositoryData, ThrowOnError>) => {
options?: Options<CreateRepositoryData, ThrowOnError>, return (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories',
return (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/repositories", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options?.headers
"Content-Type": "application/json", }
...options?.headers, });
},
});
}; };
/** /**
* List all configured rclone remotes on the host system * List all configured rclone remotes on the host system
*/ */
export const listRcloneRemotes = <ThrowOnError extends boolean = false>( export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>) => {
options?: Options<ListRcloneRemotesData, ThrowOnError>, return (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/rclone-remotes',
return (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/rclone-remotes", });
...options,
});
}; };
/** /**
* Delete a repository * Delete a repository
*/ */
export const deleteRepository = <ThrowOnError extends boolean = false>( export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>) => {
options: Options<DeleteRepositoryData, ThrowOnError>, return (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}',
return (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/{name}", });
...options,
});
}; };
/** /**
* Get a single repository by name * Get a single repository by name
*/ */
export const getRepository = <ThrowOnError extends boolean = false>( export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>) => {
options: Options<GetRepositoryData, ThrowOnError>, return (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}',
return (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/{name}", });
...options,
});
}; };
/** /**
* List all snapshots in a repository * List all snapshots in a repository
*/ */
export const listSnapshots = <ThrowOnError extends boolean = false>( export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>) => {
options: Options<ListSnapshotsData, ThrowOnError>, return (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}/snapshots',
return (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/{name}/snapshots", });
...options,
});
}; };
/** /**
* Get details of a specific snapshot * Get details of a specific snapshot
*/ */
export const getSnapshotDetails = <ThrowOnError extends boolean = false>( export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>) => {
options: Options<GetSnapshotDetailsData, ThrowOnError>, return (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}/snapshots/{snapshotId}',
return (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/{name}/snapshots/{snapshotId}", });
...options,
});
}; };
/** /**
* List files and directories in a snapshot * List files and directories in a snapshot
*/ */
export const listSnapshotFiles = <ThrowOnError extends boolean = false>( export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>) => {
options: Options<ListSnapshotFilesData, ThrowOnError>, return (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files',
return (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/{name}/snapshots/{snapshotId}/files", });
...options,
});
}; };
/** /**
* Restore a snapshot to a target path on the filesystem * Restore a snapshot to a target path on the filesystem
*/ */
export const restoreSnapshot = <ThrowOnError extends boolean = false>( export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>) => {
options: Options<RestoreSnapshotData, ThrowOnError>, return (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}/restore',
return (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/repositories/{name}/restore", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options.headers
"Content-Type": "application/json", }
...options.headers, });
},
});
}; };
/** /**
* Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors. * Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors.
*/ */
export const doctorRepository = <ThrowOnError extends boolean = false>( export const doctorRepository = <ThrowOnError extends boolean = false>(options: Options<DoctorRepositoryData, ThrowOnError>) => {
options: Options<DoctorRepositoryData, ThrowOnError>, return (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/repositories/{name}/doctor',
return (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/repositories/{name}/doctor", });
...options,
});
}; };
/** /**
* List all backup schedules * List all backup schedules
*/ */
export const listBackupSchedules = <ThrowOnError extends boolean = false>( export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>) => {
options?: Options<ListBackupSchedulesData, ThrowOnError>, return (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups',
return (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/backups", });
...options,
});
}; };
/** /**
* Create a new backup schedule for a volume * Create a new backup schedule for a volume
*/ */
export const createBackupSchedule = <ThrowOnError extends boolean = false>( export const createBackupSchedule = <ThrowOnError extends boolean = false>(options?: Options<CreateBackupScheduleData, ThrowOnError>) => {
options?: Options<CreateBackupScheduleData, ThrowOnError>, return (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups',
return (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/backups", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options?.headers
"Content-Type": "application/json", }
...options?.headers, });
},
});
}; };
/** /**
* Delete a backup schedule * Delete a backup schedule
*/ */
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>( export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>) => {
options: Options<DeleteBackupScheduleData, ThrowOnError>, return (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups/{scheduleId}',
return (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/backups/{scheduleId}", });
...options,
});
}; };
/** /**
* Get a backup schedule by ID * Get a backup schedule by ID
*/ */
export const getBackupSchedule = <ThrowOnError extends boolean = false>( export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>) => {
options: Options<GetBackupScheduleData, ThrowOnError>, return (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups/{scheduleId}',
return (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/backups/{scheduleId}", });
...options,
});
}; };
/** /**
* Update a backup schedule * Update a backup schedule
*/ */
export const updateBackupSchedule = <ThrowOnError extends boolean = false>( export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>) => {
options: Options<UpdateBackupScheduleData, ThrowOnError>, return (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups/{scheduleId}',
return (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/backups/{scheduleId}", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options.headers
"Content-Type": "application/json", }
...options.headers, });
},
});
}; };
/** /**
* Get a backup schedule for a specific volume * Get a backup schedule for a specific volume
*/ */
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>( export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>) => {
options: Options<GetBackupScheduleForVolumeData, ThrowOnError>, return (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups/volume/{volumeId}',
return (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/backups/volume/{volumeId}", });
...options,
});
}; };
/** /**
* Trigger a backup immediately for a schedule * Trigger a backup immediately for a schedule
*/ */
export const runBackupNow = <ThrowOnError extends boolean = false>( export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>) => {
options: Options<RunBackupNowData, ThrowOnError>, return (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/backups/{scheduleId}/run',
return (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/backups/{scheduleId}/run", });
...options,
});
}; };
/** /**
* Stop a backup that is currently in progress * Stop a backup that is currently in progress
*/ */
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => { export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => {
return (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({ return (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({
url: "/api/v1/backups/{scheduleId}/stop", url: '/api/v1/backups/{scheduleId}/stop',
...options, ...options
}); });
};
/**
* Manually apply retention policy to clean up old snapshots
*/
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => {
return (options.client ?? client).post<RunForgetResponses, RunForgetErrors, ThrowOnError>({
url: '/api/v1/backups/{scheduleId}/forget',
...options
});
}; };
/** /**
* Get system information including available capabilities * Get system information including available capabilities
*/ */
export const getSystemInfo = <ThrowOnError extends boolean = false>( export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>) => {
options?: Options<GetSystemInfoData, ThrowOnError>, return (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/system/info',
return (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({ ...options
url: "/api/v1/system/info", });
...options,
});
}; };
/** /**
* Download the Restic password file for backup recovery. Requires password re-authentication. * Download the Restic password file for backup recovery. Requires password re-authentication.
*/ */
export const downloadResticPassword = <ThrowOnError extends boolean = false>( export const downloadResticPassword = <ThrowOnError extends boolean = false>(options?: Options<DownloadResticPasswordData, ThrowOnError>) => {
options?: Options<DownloadResticPasswordData, ThrowOnError>, return (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
) => { url: '/api/v1/system/restic-password',
return (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({ ...options,
url: "/api/v1/system/restic-password", headers: {
...options, 'Content-Type': 'application/json',
headers: { ...options?.headers
"Content-Type": "application/json", }
...options?.headers, });
},
});
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { arktypeResolver } from "@hookform/resolvers/arktype"; import { arktypeResolver } from "@hookform/resolvers/arktype";
import { type } from "arktype"; import { type } from "arktype";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { cn, slugify } from "~/client/lib/utils"; import { cn, slugify } from "~/client/lib/utils";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
@@ -15,6 +15,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { useSystemInfo } from "~/client/hooks/use-system-info"; import { useSystemInfo } from "~/client/hooks/use-system-info";
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic"; import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen"; import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
import { Checkbox } from "./ui/checkbox";
export const formSchema = type({ export const formSchema = type({
name: "2<=string<=32", name: "2<=string<=32",
@@ -36,6 +37,7 @@ type Props = {
const defaultValuesForType = { const defaultValuesForType = {
local: { backend: "local" as const, compressionMode: "auto" as const }, local: { backend: "local" as const, compressionMode: "auto" as const },
s3: { backend: "s3" as const, compressionMode: "auto" as const }, s3: { backend: "s3" as const, compressionMode: "auto" as const },
r2: { backend: "r2" as const, compressionMode: "auto" as const },
gcs: { backend: "gcs" as const, compressionMode: "auto" as const }, gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
azure: { backend: "azure" as const, compressionMode: "auto" as const }, azure: { backend: "azure" as const, compressionMode: "auto" as const },
rclone: { backend: "rclone" as const, compressionMode: "auto" as const }, rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
@@ -58,9 +60,12 @@ export const CreateRepositoryForm = ({
}, },
}); });
const { watch } = form; const { watch, setValue } = form;
const watchedBackend = watch("backend"); const watchedBackend = watch("backend");
const watchedIsExistingRepository = watch("isExistingRepository");
const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default");
const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({ const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({
...listRcloneRemotesOptions(), ...listRcloneRemotesOptions(),
@@ -69,6 +74,8 @@ export const CreateRepositoryForm = ({
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
name: form.getValues().name, name: form.getValues().name,
isExistingRepository: form.getValues().isExistingRepository,
customPassword: form.getValues().customPassword,
...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType], ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
}); });
}, [watchedBackend, form]); }, [watchedBackend, form]);
@@ -115,6 +122,7 @@ export const CreateRepositoryForm = ({
<SelectContent> <SelectContent>
<SelectItem value="local">Local</SelectItem> <SelectItem value="local">Local</SelectItem>
<SelectItem value="s3">S3</SelectItem> <SelectItem value="s3">S3</SelectItem>
<SelectItem value="r2">Cloudflare R2</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem> <SelectItem value="gcs">Google Cloud Storage</SelectItem>
<SelectItem value="azure">Azure Blob Storage</SelectItem> <SelectItem value="azure">Azure Blob Storage</SelectItem>
<Tooltip> <Tooltip>
@@ -161,6 +169,81 @@ export const CreateRepositoryForm = ({
)} )}
/> />
<FormField
control={form.control}
name="isExistingRepository"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
if (!checked) {
setPasswordMode("default");
setValue("customPassword", undefined);
}
}}
/>
</FormControl>
<div className="space-y-1">
<FormLabel>Import existing repository</FormLabel>
<FormDescription>Check this if the repository already exists at the specified location</FormDescription>
</div>
</FormItem>
)}
/>
{watchedIsExistingRepository && (
<>
<FormItem>
<FormLabel>Repository Password</FormLabel>
<Select
onValueChange={(value) => {
setPasswordMode(value as "default" | "custom");
if (value === "default") {
setValue("customPassword", undefined);
}
}}
defaultValue={passwordMode}
value={passwordMode}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select password option" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="default">Use Ironmount's password</SelectItem>
<SelectItem value="custom">Enter password manually</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose whether to use Ironmount's master password or enter a custom password for the existing
repository.
</FormDescription>
</FormItem>
{passwordMode === "custom" && (
<FormField
control={form.control}
name="customPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Repository Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter repository password" {...field} />
</FormControl>
<FormDescription>
The password used to encrypt this repository. It will be stored securely.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
</>
)}
{watchedBackend === "s3" && ( {watchedBackend === "s3" && (
<> <>
<FormField <FormField
@@ -222,6 +305,69 @@ export const CreateRepositoryForm = ({
</> </>
)} )}
{watchedBackend === "r2" && (
<>
<FormField
control={form.control}
name="endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint</FormLabel>
<FormControl>
<Input placeholder="<account-id>.r2.cloudflarestorage.com" {...field} />
</FormControl>
<FormDescription>
R2 endpoint (without https://). Find in R2 dashboard under bucket settings.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="my-backup-bucket" {...field} />
</FormControl>
<FormDescription>R2 bucket name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accessKeyId"
render={({ field }) => (
<FormItem>
<FormLabel>Access Key ID</FormLabel>
<FormControl>
<Input placeholder="Access Key ID from R2 API tokens" {...field} />
</FormControl>
<FormDescription>R2 API token Access Key ID (create in Cloudflare R2 dashboard).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="secretAccessKey"
render={({ field }) => (
<FormItem>
<FormLabel>Secret Access Key</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>R2 API token Secret Access Key (shown once when creating token).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{watchedBackend === "gcs" && ( {watchedBackend === "gcs" && (
<> <>
<FormField <FormField

View File

@@ -536,42 +536,44 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
</> </>
)} )}
<div className="space-y-3"> {watchedBackend !== "directory" && (
<div className="flex items-center gap-2"> <div className="space-y-3">
<Button <div className="flex items-center gap-2">
type="button" <Button
variant="outline" type="button"
onClick={handleTestConnection} variant="outline"
disabled={testBackendConnection.isPending} onClick={handleTestConnection}
className="flex-1" disabled={testBackendConnection.isPending}
> className="flex-1"
{testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} >
{!testBackendConnection.isPending && testMessage?.success && ( {testBackendConnection.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<CheckCircle className="mr-2 h-4 w-4 text-green-500" /> {!testBackendConnection.isPending && testMessage?.success && (
)} <CheckCircle className="mr-2 h-4 w-4 text-green-500" />
{!testBackendConnection.isPending && testMessage && !testMessage.success && ( )}
<XCircle className="mr-2 h-4 w-4 text-red-500" /> {!testBackendConnection.isPending && testMessage && !testMessage.success && (
)} <XCircle className="mr-2 h-4 w-4 text-red-500" />
{testBackendConnection.isPending )}
? "Testing..." {testBackendConnection.isPending
: testMessage ? "Testing..."
? testMessage.success : testMessage
? "Connection Successful" ? testMessage.success
: "Test Failed" ? "Connection Successful"
: "Test Connection"} : "Test Failed"
</Button> : "Test Connection"}
</div> </Button>
{testMessage && (
<div
className={cn("text-xs p-2 rounded-md text-wrap wrap-anywhere", {
"bg-green-50 text-green-700 border border-green-200": testMessage.success,
"bg-red-50 text-red-700 border border-red-200": !testMessage.success,
})}
>
{testMessage.message}
</div> </div>
)} {testMessage && (
</div> <div
className={cn("text-xs p-2 rounded-md text-wrap wrap-anywhere", {
"bg-green-50 text-green-700 border border-green-200": testMessage.success,
"bg-red-50 text-red-700 border border-red-200": !testMessage.success,
})}
>
{testMessage.message}
</div>
)}
</div>
)}
{mode === "update" && ( {mode === "update" && (
<Button type="submit" className="w-full" loading={loading}> <Button type="submit" className="w-full" loading={loading}>
Save Changes Save Changes

View File

@@ -254,7 +254,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
<CardHeader> <CardHeader>
<CardTitle>Backup paths</CardTitle> <CardTitle>Backup paths</CardTitle>
<CardDescription> <CardDescription>
Select which folders to include in the backup. If no paths are selected, the entire volume will be Select which folders or files to include in the backup. If no paths are selected, the entire volume will be
backed up. backed up.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
@@ -264,7 +264,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
selectedPaths={selectedPaths} selectedPaths={selectedPaths}
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}
withCheckboxes={true} withCheckboxes={true}
foldersOnly={true} foldersOnly={false}
className="flex-1 border rounded-md bg-card p-2 min-h-[300px] max-h-[400px] overflow-auto" className="flex-1 border rounded-md bg-card p-2 min-h-[300px] max-h-[400px] overflow-auto"
/> />
{selectedPaths.size > 0 && ( {selectedPaths.size > 0 && (

View File

@@ -1,4 +1,4 @@
import { Pencil, Play, Square, Trash2 } from "lucide-react"; import { Eraser, Pencil, Play, Square, Trash2 } from "lucide-react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { OnOff } from "~/client/components/onoff"; import { OnOff } from "~/client/components/onoff";
import { Button } from "~/client/components/ui/button"; import { Button } from "~/client/components/ui/button";
@@ -14,6 +14,10 @@ import {
} from "~/client/components/ui/alert-dialog"; } from "~/client/components/ui/alert-dialog";
import type { BackupSchedule } from "~/client/lib/types"; import type { BackupSchedule } from "~/client/lib/types";
import { BackupProgressCard } from "./backup-progress-card"; import { BackupProgressCard } from "./backup-progress-card";
import { runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { parseError } from "~/client/lib/errors";
type Props = { type Props = {
schedule: BackupSchedule; schedule: BackupSchedule;
@@ -28,6 +32,17 @@ export const ScheduleSummary = (props: Props) => {
const { schedule, handleToggleEnabled, handleRunBackupNow, handleStopBackup, handleDeleteSchedule, setIsEditMode } = const { schedule, handleToggleEnabled, handleRunBackupNow, handleStopBackup, handleDeleteSchedule, setIsEditMode } =
props; props;
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showForgetConfirm, setShowForgetConfirm] = useState(false);
const runForget = useMutation({
...runForgetMutation(),
onSuccess: () => {
toast.success("Retention policy applied successfully");
},
onError: (error) => {
toast.error("Failed to apply retention policy", { description: parseError(error)?.message });
},
});
const summary = useMemo(() => { const summary = useMemo(() => {
const scheduleLabel = schedule ? schedule.cronExpression : "-"; const scheduleLabel = schedule ? schedule.cronExpression : "-";
@@ -56,6 +71,11 @@ export const ScheduleSummary = (props: Props) => {
handleDeleteSchedule(); handleDeleteSchedule();
}; };
const handleConfirmForget = () => {
setShowForgetConfirm(false);
runForget.mutate({ path: { scheduleId: schedule.id.toString() } });
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<Card> <Card>
@@ -89,6 +109,18 @@ export const ScheduleSummary = (props: Props) => {
<span className="sm:inline">Backup now</span> <span className="sm:inline">Backup now</span>
</Button> </Button>
)} )}
{schedule.retentionPolicy && (
<Button
variant="outline"
size="sm"
loading={runForget.isPending}
onClick={() => setShowForgetConfirm(true)}
className="w-full sm:w-auto"
>
<Eraser className="h-4 w-4 mr-2" />
<span className="sm:inline">Run cleanup</span>
</Button>
)}
<Button variant="outline" size="sm" onClick={() => setIsEditMode(true)} className="w-full sm:w-auto"> <Button variant="outline" size="sm" onClick={() => setIsEditMode(true)} className="w-full sm:w-auto">
<Pencil className="h-4 w-4 mr-2" /> <Pencil className="h-4 w-4 mr-2" />
<span className="sm:inline">Edit schedule</span> <span className="sm:inline">Edit schedule</span>
@@ -167,6 +199,22 @@ export const ScheduleSummary = (props: Props) => {
</div> </div>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog open={showForgetConfirm} onOpenChange={setShowForgetConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Run retention policy cleanup?</AlertDialogTitle>
<AlertDialogDescription>
This will apply the retention policy and permanently delete old snapshots according to the configured
rules ({summary.retentionLabel}). This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex gap-3 justify-end">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleConfirmForget}>Run cleanup</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { type } from "arktype";
export const REPOSITORY_BACKENDS = { export const REPOSITORY_BACKENDS = {
local: "local", local: "local",
s3: "s3", s3: "s3",
r2: "r2",
gcs: "gcs", gcs: "gcs",
azure: "azure", azure: "azure",
rclone: "rclone", rclone: "rclone",
@@ -10,25 +11,39 @@ export const REPOSITORY_BACKENDS = {
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
// Common fields for all repository configs
const baseRepositoryConfigSchema = type({
isExistingRepository: "boolean?",
customPassword: "string?",
});
export const s3RepositoryConfigSchema = type({ export const s3RepositoryConfigSchema = type({
backend: "'s3'", backend: "'s3'",
endpoint: "string", endpoint: "string",
bucket: "string", bucket: "string",
accessKeyId: "string", accessKeyId: "string",
secretAccessKey: "string", secretAccessKey: "string",
}); }).and(baseRepositoryConfigSchema);
export const r2RepositoryConfigSchema = type({
backend: "'r2'",
endpoint: "string",
bucket: "string",
accessKeyId: "string",
secretAccessKey: "string",
}).and(baseRepositoryConfigSchema);
export const localRepositoryConfigSchema = type({ export const localRepositoryConfigSchema = type({
backend: "'local'", backend: "'local'",
name: "string", name: "string",
}); }).and(baseRepositoryConfigSchema);
export const gcsRepositoryConfigSchema = type({ export const gcsRepositoryConfigSchema = type({
backend: "'gcs'", backend: "'gcs'",
bucket: "string", bucket: "string",
projectId: "string", projectId: "string",
credentialsJson: "string", credentialsJson: "string",
}); }).and(baseRepositoryConfigSchema);
export const azureRepositoryConfigSchema = type({ export const azureRepositoryConfigSchema = type({
backend: "'azure'", backend: "'azure'",
@@ -36,15 +51,16 @@ export const azureRepositoryConfigSchema = type({
accountName: "string", accountName: "string",
accountKey: "string", accountKey: "string",
endpointSuffix: "string?", endpointSuffix: "string?",
}); }).and(baseRepositoryConfigSchema);
export const rcloneRepositoryConfigSchema = type({ export const rcloneRepositoryConfigSchema = type({
backend: "'rclone'", backend: "'rclone'",
remote: "string", remote: "string",
path: "string", path: "string",
}); }).and(baseRepositoryConfigSchema);
export const repositoryConfigSchema = s3RepositoryConfigSchema export const repositoryConfigSchema = s3RepositoryConfigSchema
.or(r2RepositoryConfigSchema)
.or(localRepositoryConfigSchema) .or(localRepositoryConfigSchema)
.or(gcsRepositoryConfigSchema) .or(gcsRepositoryConfigSchema)
.or(azureRepositoryConfigSchema) .or(azureRepositoryConfigSchema)

View File

@@ -19,7 +19,9 @@ export class CleanupDanglingMountsJob extends Job {
const matchingVolume = allVolumes.find((v) => getVolumePath(v) === mount.mountPoint); const matchingVolume = allVolumes.find((v) => getVolumePath(v) === mount.mountPoint);
if (!matchingVolume) { if (!matchingVolume) {
logger.info(`Found dangling mount at ${mount.mountPoint}, attempting to unmount...`); logger.info(`Found dangling mount at ${mount.mountPoint}, attempting to unmount...`);
await executeUnmount(mount.mountPoint); await executeUnmount(mount.mountPoint).catch((err) => {
logger.warn(`Failed to unmount dangling mount ${mount.mountPoint}: ${toMessage(err)}`);
});
await fs.rmdir(path.dirname(mount.mountPoint)).catch((err) => { await fs.rmdir(path.dirname(mount.mountPoint)).catch((err) => {
logger.warn( logger.warn(

View File

@@ -13,6 +13,10 @@ export const executeMount = async (args: string[]): Promise<void> => {
if (stderr?.trim()) { if (stderr?.trim()) {
logger.warn(stderr.trim()); logger.warn(stderr.trim());
} }
if (result.exitCode !== 0) {
throw new Error(`Mount command failed with exit code ${result.exitCode}: ${stderr?.trim()}`);
}
}; };
export const executeUnmount = async (path: string): Promise<void> => { export const executeUnmount = async (path: string): Promise<void> => {
@@ -24,6 +28,10 @@ export const executeUnmount = async (path: string): Promise<void> => {
if (stderr?.trim()) { if (stderr?.trim()) {
logger.warn(stderr.trim()); logger.warn(stderr.trim());
} }
if (result.exitCode !== 0) {
throw new Error(`Mount command failed with exit code ${result.exitCode}: ${stderr?.trim()}`);
}
}; };
export const createTestFile = async (path: string): Promise<void> => { export const createTestFile = async (path: string): Promise<void> => {

View File

@@ -8,6 +8,7 @@ import {
getBackupScheduleForVolumeDto, getBackupScheduleForVolumeDto,
listBackupSchedulesDto, listBackupSchedulesDto,
runBackupNowDto, runBackupNowDto,
runForgetDto,
stopBackupDto, stopBackupDto,
updateBackupScheduleDto, updateBackupScheduleDto,
updateBackupScheduleBody, updateBackupScheduleBody,
@@ -17,6 +18,7 @@ import {
type GetBackupScheduleForVolumeResponseDto, type GetBackupScheduleForVolumeResponseDto,
type ListBackupSchedulesResponseDto, type ListBackupSchedulesResponseDto,
type RunBackupNowDto, type RunBackupNowDto,
type RunForgetDto,
type StopBackupDto, type StopBackupDto,
type UpdateBackupScheduleDto, type UpdateBackupScheduleDto,
} from "./backups.dto"; } from "./backups.dto";
@@ -78,4 +80,11 @@ export const backupScheduleController = new Hono()
await backupsService.stopBackup(Number(scheduleId)); await backupsService.stopBackup(Number(scheduleId));
return c.json<StopBackupDto>({ success: true }, 200); return c.json<StopBackupDto>({ success: true }, 200);
})
.post("/:scheduleId/forget", runForgetDto, async (c) => {
const scheduleId = c.req.param("scheduleId");
await backupsService.runForget(Number(scheduleId));
return c.json<RunForgetDto>({ success: true }, 200);
}); });

View File

@@ -251,3 +251,28 @@ export const stopBackupDto = describeRoute({
}, },
}, },
}); });
/**
* Run retention policy (forget) manually
*/
export const runForgetResponse = type({
success: "boolean",
});
export type RunForgetDto = typeof runForgetResponse.infer;
export const runForgetDto = describeRoute({
description: "Manually apply retention policy to clean up old snapshots",
operationId: "runForget",
tags: ["Backups"],
responses: {
200: {
description: "Retention policy applied successfully",
content: {
"application/json": {
schema: resolver(runForgetResponse),
},
},
},
},
});

View File

@@ -195,9 +195,11 @@ const executeBackup = async (scheduleId: number, manual = false) => {
repositoryName: repository.name, repositoryName: repository.name,
}); });
const nextBackupAt = calculateNextRun(schedule.cronExpression);
await db await db
.update(backupSchedulesTable) .update(backupSchedulesTable)
.set({ lastBackupStatus: "in_progress", updatedAt: Date.now(), lastBackupError: null }) .set({ lastBackupStatus: "in_progress", updatedAt: Date.now(), lastBackupError: null, nextBackupAt })
.where(eq(backupSchedulesTable.id, scheduleId)); .where(eq(backupSchedulesTable.id, scheduleId));
const abortController = new AbortController(); const abortController = new AbortController();
@@ -340,6 +342,32 @@ const stopBackup = async (scheduleId: number) => {
abortController.abort(); abortController.abort();
}; };
const runForget = async (scheduleId: number) => {
const schedule = await db.query.backupSchedulesTable.findFirst({
where: eq(backupSchedulesTable.id, scheduleId),
});
if (!schedule) {
throw new NotFoundError("Backup schedule not found");
}
if (!schedule.retentionPolicy) {
throw new BadRequestError("No retention policy configured for this schedule");
}
const repository = await db.query.repositoriesTable.findFirst({
where: eq(repositoriesTable.id, schedule.repositoryId),
});
if (!repository) {
throw new NotFoundError("Repository not found");
}
logger.info(`Manually running retention policy (forget) for schedule ${scheduleId}`);
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
logger.info(`Retention policy applied successfully for schedule ${scheduleId}`);
};
export const backupsService = { export const backupsService = {
listSchedules, listSchedules,
getSchedule, getSchedule,
@@ -350,4 +378,5 @@ export const backupsService = {
getSchedulesToExecute, getSchedulesToExecute,
getScheduleForVolume, getScheduleForVolume,
stopBackup, stopBackup,
runForget,
}; };

View File

@@ -15,10 +15,15 @@ const listRepositories = async () => {
}; };
const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => { const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => {
const encryptedConfig: Record<string, string> = { ...config }; const encryptedConfig: Record<string, string | boolean> = { ...config };
if (config.customPassword) {
encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
}
switch (config.backend) { switch (config.backend) {
case "s3": case "s3":
case "r2":
encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId); encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey); encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
break; break;
@@ -64,16 +69,24 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
throw new InternalServerError("Failed to create repository"); throw new InternalServerError("Failed to create repository");
} }
const { success, error } = await restic.init(encryptedConfig); let error: string | null = null;
if (success) { if (config.isExistingRepository) {
const result = await restic
.snapshots(encryptedConfig)
.then(() => ({ error: null }))
.catch((error) => ({ error }));
error = result.error;
} else {
const initResult = await restic.init(encryptedConfig);
error = initResult.error;
}
if (!error) {
await db await db
.update(repositoriesTable) .update(repositoriesTable)
.set({ .set({ status: "healthy", lastChecked: Date.now(), lastError: null })
status: "healthy",
lastChecked: Date.now(),
lastError: null,
})
.where(eq(repositoriesTable.id, id)); .where(eq(repositoriesTable.id, id));
return { repository: created, status: 201 }; return { repository: created, status: 201 };

View File

@@ -74,6 +74,10 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
return `${REPOSITORY_BASE}/${config.name}`; return `${REPOSITORY_BASE}/${config.name}`;
case "s3": case "s3":
return `s3:${config.endpoint}/${config.bucket}`; return `s3:${config.endpoint}/${config.bucket}`;
case "r2": {
const endpoint = config.endpoint.replace(/^https?:\/\//, "");
return `s3:${endpoint}/${config.bucket}`;
}
case "gcs": case "gcs":
return `gs:${config.bucket}:/`; return `gs:${config.bucket}:/`;
case "azure": case "azure":
@@ -89,18 +93,33 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
const buildEnv = async (config: RepositoryConfig) => { const buildEnv = async (config: RepositoryConfig) => {
const env: Record<string, string> = { const env: Record<string, string> = {
RESTIC_CACHE_DIR: "/var/lib/ironmount/restic/cache", RESTIC_CACHE_DIR: "/var/lib/ironmount/restic/cache",
RESTIC_PASSWORD_FILE: RESTIC_PASS_FILE,
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin", PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
}; };
if (config.isExistingRepository && config.customPassword) {
const decryptedPassword = await cryptoUtils.decrypt(config.customPassword);
const passwordFilePath = path.join("/tmp", `ironmount-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
await fs.writeFile(passwordFilePath, decryptedPassword, { mode: 0o600 });
env.RESTIC_PASSWORD_FILE = passwordFilePath;
} else {
env.RESTIC_PASSWORD_FILE = RESTIC_PASS_FILE;
}
switch (config.backend) { switch (config.backend) {
case "s3": case "s3":
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId); env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey); env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
break; break;
case "r2":
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
env.AWS_REGION = "auto";
env.AWS_S3_FORCE_PATH_STYLE = "true";
break;
case "gcs": { case "gcs": {
const decryptedCredentials = await cryptoUtils.decrypt(config.credentialsJson); const decryptedCredentials = await cryptoUtils.decrypt(config.credentialsJson);
const credentialsPath = path.join("/tmp", `gcs-credentials-${crypto.randomBytes(8).toString("hex")}.json`); const credentialsPath = path.join("/tmp", `ironmount-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
await fs.writeFile(credentialsPath, decryptedCredentials, { mode: 0o600 }); await fs.writeFile(credentialsPath, decryptedCredentials, { mode: 0o600 });
env.GOOGLE_PROJECT_ID = config.projectId; env.GOOGLE_PROJECT_ID = config.projectId;
env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath; env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
@@ -129,7 +148,7 @@ const init = async (config: RepositoryConfig) => {
if (res.exitCode !== 0) { if (res.exitCode !== 0) {
logger.error(`Restic init failed: ${res.stderr}`); logger.error(`Restic init failed: ${res.stderr}`);
return { success: false, error: res.stderr }; return { success: false, error: res.stderr.toString() };
} }
logger.info(`Restic repository initialized: ${repoUrl}`); logger.info(`Restic repository initialized: ${repoUrl}`);

View File

@@ -19,6 +19,9 @@ services:
- ./app:/app/app - ./app:/app/app
- ~/.config/rclone:/root/.config/rclone - ~/.config/rclone:/root/.config/rclone
- /var/lib/ironmount:/var/lib/ironmount:rshared
- /run/docker/plugins:/run/docker/plugins
- /var/run/docker.sock:/var/run/docker.sock
ironmount-prod: ironmount-prod:
build: build:

View File

@@ -1 +0,0 @@
proj_Nwis7nYU1DiPGTtNlwRKBVtdgo5cOWPsnwbtxj2Urg0

View File

@@ -1,4 +0,0 @@
docker run --rm -it -v nicolas:/data alpine sh -lc 'echo hello > /data/hi && cat /data/hi'
mount -t davfs http://192.168.2.42 /mnt/webdav