Wire up GeoLite2-City geoip databases into dashboard. #64

This commit is contained in:
Sebastian Jeltsch
2025-05-29 23:59:20 +02:00
parent 5a61369c50
commit 271407e51e
7 changed files with 1456 additions and 23 deletions

View File

@@ -0,0 +1,899 @@
export declare function addCronCallback(name: string, schedule: string, cb: () => void | Promise<void>): void;
export declare function addPeriodicCallback(milliseconds: number, cb: (cancel: () => void) => void): () => void;
export declare function addRoute(method: Method, route: string, callback: CallbackType): void;
export declare type CallbackType = (req: RequestType) => MaybeResponse<ResponseType_2>;
declare namespace Deno_2 {
interface ReadFileOptions {
/**
* An abort signal to allow cancellation of the file read operation.
* If the signal becomes aborted the readFile operation will be stopped
* and the promise returned will be rejected with an AbortError.
*/
signal?: AbortSignal;
}
interface WriteFileOptions {
/** If set to `true`, will append to a file instead of overwriting previous
* contents.
*
* @default {false} */
append?: boolean;
/** Sets the option to allow creating a new file, if one doesn't already
* exist at the specified path.
*
* @default {true} */
create?: boolean;
/** If set to `true`, no file, directory, or symlink is allowed to exist at
* the target location. When createNew is set to `true`, `create` is ignored.
*
* @default {false} */
createNew?: boolean;
/** Permissions always applied to file. */
mode?: number;
/** An abort signal to allow cancellation of the file write operation.
*
* If the signal becomes aborted the write file operation will be stopped
* and the promise returned will be rejected with an {@linkcode AbortError}.
*/
signal?: AbortSignal;
}
/**
* Options which can be set when using {@linkcode Deno.makeTempDir},
* {@linkcode Deno.makeTempDirSync}, {@linkcode Deno.makeTempFile}, and
* {@linkcode Deno.makeTempFileSync}.
*
* @category File System */
interface MakeTempOptions {
/** Directory where the temporary directory should be created (defaults to
* the env variable `TMPDIR`, or the system's default, usually `/tmp`).
*
* Note that if the passed `dir` is relative, the path returned by
* `makeTempFile()` and `makeTempDir()` will also be relative. Be mindful of
* this when changing working directory. */
dir?: string;
/** String that should precede the random portion of the temporary
* directory's name. */
prefix?: string;
/** String that should follow the random portion of the temporary
* directory's name. */
suffix?: string;
}
/**
* Options which can be set when using {@linkcode Deno.mkdir} and
* {@linkcode Deno.mkdirSync}.
*
* @category File System */
interface MkdirOptions {
/** If set to `true`, means that any intermediate directories will also be
* created (as with the shell command `mkdir -p`).
*
* Intermediate directories are created with the same permissions.
*
* When recursive is set to `true`, succeeds silently (without changing any
* permissions) if a directory already exists at the path, or if the path
* is a symlink to an existing directory.
*
* @default {false} */
recursive?: boolean;
/** Permissions to use when creating the directory (defaults to `0o777`,
* before the process's umask).
*
* Ignored on Windows. */
mode?: number;
}
/**
* Information about a directory entry returned from {@linkcode Deno.readDir}
* and {@linkcode Deno.readDirSync}.
*
* @category File System */
interface DirEntry {
/** The file name of the entry. It is just the entity name and does not
* include the full path. */
name: string;
/** True if this is info for a regular file. Mutually exclusive to
* `DirEntry.isDirectory` and `DirEntry.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `DirEntry.isFile` and `DirEntry.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `DirEntry.isFile` and `DirEntry.isDirectory`. */
isSymlink: boolean;
}
/**
* Options which can be set when doing {@linkcode Deno.open} and
* {@linkcode Deno.openSync}.
*
* @category File System */
interface OpenOptions {
/** Sets the option for read access. This option, when `true`, means that
* the file should be read-able if opened.
*
* @default {true} */
read?: boolean;
/** Sets the option for write access. This option, when `true`, means that
* the file should be write-able if opened. If the file already exists,
* any write calls on it will overwrite its contents, by default without
* truncating it.
*
* @default {false} */
write?: boolean;
/** Sets the option for the append mode. This option, when `true`, means
* that writes will append to a file instead of overwriting previous
* contents.
*
* Note that setting `{ write: true, append: true }` has the same effect as
* setting only `{ append: true }`.
*
* @default {false} */
append?: boolean;
/** Sets the option for truncating a previous file. If a file is
* successfully opened with this option set it will truncate the file to `0`
* size if it already exists. The file must be opened with write access
* for truncate to work.
*
* @default {false} */
truncate?: boolean;
/** Sets the option to allow creating a new file, if one doesn't already
* exist at the specified path. Requires write or append access to be
* used.
*
* @default {false} */
create?: boolean;
/** If set to `true`, no file, directory, or symlink is allowed to exist at
* the target location. Requires write or append access to be used. When
* createNew is set to `true`, create and truncate are ignored.
*
* @default {false} */
createNew?: boolean;
/** Permissions to use if creating the file (defaults to `0o666`, before
* the process's umask).
*
* Ignored on Windows. */
mode?: number;
}
/**
* Options which can be set when using {@linkcode Deno.remove} and
* {@linkcode Deno.removeSync}.
*
* @category File System */
interface RemoveOptions {
/** If set to `true`, path will be removed even if it's a non-empty directory.
*
* @default {false} */
recursive?: boolean;
}
/** Options that can be used with {@linkcode symlink} and
* {@linkcode symlinkSync}.
*
* @category File System */
interface SymlinkOptions {
/** Specify the symbolic link type as file, directory or NTFS junction. This
* option only applies to Windows and is ignored on other operating systems. */
type: "file" | "dir" | "junction";
}
function writeFile(path: string | URL, data: Uint8Array | ReadableStream<Uint8Array>, options?: WriteFileOptions): Promise<void>;
function writeTextFile(path: string | URL, data: string | ReadableStream<string>, options?: WriteFileOptions): Promise<void>;
function readTextFile(path: string | URL, options?: ReadFileOptions): Promise<string>;
function readFile(path: string | URL, options?: ReadFileOptions): Promise<Uint8Array>;
function chmod(path: string | URL, mode: number): Promise<void>;
function chown(path: string | URL, uid: number | null, gid: number | null): Promise<void>;
function cwd(): string;
function makeTempDir(options?: MakeTempOptions): Promise<string>;
function makeTempFile(options?: MakeTempOptions): Promise<string>;
function mkdir(path: string | URL, options?: MkdirOptions): Promise<void>;
function chdir(directory: string | URL): void;
function copyFile(fromPath: string | URL, toPath: string | URL): Promise<void>;
function readDir(path: string | URL): AsyncIterable<DirEntry>;
function readLink(path: string | URL): Promise<string>;
function realPath(path: string | URL): Promise<string>;
function remove(path: string | URL, options?: RemoveOptions): Promise<void>;
function rename(oldpath: string | URL, newpath: string | URL): Promise<void>;
function stat(path: string | URL): Promise<FileInfo>;
function lstat(path: string | URL): Promise<FileInfo>;
function truncate(name: string, len?: number): Promise<void>;
function open(path: string | URL, options?: OpenOptions): Promise<FsFile>;
function create(path: string | URL): Promise<FsFile>;
function symlink(oldpath: string | URL, newpath: string | URL, options?: SymlinkOptions): Promise<void>;
function link(oldpath: string, newpath: string): Promise<void>;
function utime(path: string | URL, atime: number | Date, mtime: number | Date): Promise<void>;
function umask(mask?: number): number;
/** Provides information about a file and is returned by
* {@linkcode Deno.stat}, {@linkcode Deno.lstat}, {@linkcode Deno.statSync},
* and {@linkcode Deno.lstatSync} or from calling `stat()` and `statSync()`
* on an {@linkcode Deno.FsFile} instance.
*
* @category File System
*/
interface FileInfo {
/** True if this is info for a regular file. Mutually exclusive to
* `FileInfo.isDirectory` and `FileInfo.isSymlink`. */
isFile: boolean;
/** True if this is info for a regular directory. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isSymlink`. */
isDirectory: boolean;
/** True if this is info for a symlink. Mutually exclusive to
* `FileInfo.isFile` and `FileInfo.isDirectory`. */
isSymlink: boolean;
/** The size of the file, in bytes. */
size: number;
/** The last modification time of the file. This corresponds to the `mtime`
* field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This
* may not be available on all platforms. */
mtime: Date | null;
/** The last access time of the file. This corresponds to the `atime`
* field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not
* be available on all platforms. */
atime: Date | null;
/** The creation time of the file. This corresponds to the `birthtime`
* field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may
* not be available on all platforms. */
birthtime: Date | null;
/** The last change time of the file. This corresponds to the `ctime`
* field from `stat` on Mac/BSD and `ChangeTime` on Windows. This may
* not be available on all platforms. */
ctime: Date | null;
/** ID of the device containing the file. */
dev: number;
/** Inode number.
*
* _Linux/Mac OS only._ */
ino: number | null;
/** The underlying raw `st_mode` bits that contain the standard Unix
* permissions for this file/directory.
*/
mode: number | null;
/** Number of hard links pointing to this file.
*
* _Linux/Mac OS only._ */
nlink: number | null;
/** User ID of the owner of this file.
*
* _Linux/Mac OS only._ */
uid: number | null;
/** Group ID of the owner of this file.
*
* _Linux/Mac OS only._ */
gid: number | null;
/** Device ID of this file.
*
* _Linux/Mac OS only._ */
rdev: number | null;
/** Blocksize for filesystem I/O.
*
* _Linux/Mac OS only._ */
blksize: number | null;
/** Number of blocks allocated to the file, in 512-byte units.
*
* _Linux/Mac OS only._ */
blocks: number | null;
/** True if this is info for a block device.
*
* _Linux/Mac OS only._ */
isBlockDevice: boolean | null;
/** True if this is info for a char device.
*
* _Linux/Mac OS only._ */
isCharDevice: boolean | null;
/** True if this is info for a fifo.
*
* _Linux/Mac OS only._ */
isFifo: boolean | null;
/** True if this is info for a socket.
*
* _Linux/Mac OS only._ */
isSocket: boolean | null;
}
/**
* A enum which defines the seek mode for IO related APIs that support
* seeking.
*
* @category I/O */
enum SeekMode {
Start = 0,
Current = 1,
End = 2
}
/** @category I/O */
interface SetRawOptions {
/**
* The `cbreak` option can be used to indicate that characters that
* correspond to a signal should still be generated. When disabling raw
* mode, this option is ignored. This functionality currently only works on
* Linux and Mac OS.
*/
cbreak: boolean;
}
class FsFile implements Disposable {
/** A {@linkcode ReadableStream} instance representing to the byte contents
* of the file. This makes it easy to interoperate with other web streams
* based APIs.
*
* ```ts
* using file = await Deno.open("my_file.txt", { read: true });
* const decoder = new TextDecoder();
* for await (const chunk of file.readable) {
* console.log(decoder.decode(chunk));
* }
* ```
*/
readonly readable: ReadableStream<Uint8Array>;
/** A {@linkcode WritableStream} instance to write the contents of the
* file. This makes it easy to interoperate with other web streams based
* APIs.
*
* ```ts
* const items = ["hello", "world"];
* using file = await Deno.open("my_file.txt", { write: true });
* const encoder = new TextEncoder();
* const writer = file.writable.getWriter();
* for (const item of items) {
* await writer.write(encoder.encode(item));
* }
* ```
*/
readonly writable: WritableStream<Uint8Array>;
/** Write the contents of the array buffer (`p`) to the file.
*
* Resolves to the number of bytes written.
*
* **It is not guaranteed that the full buffer will be written in a single
* call.**
*
* ```ts
* const encoder = new TextEncoder();
* const data = encoder.encode("Hello world");
* using file = await Deno.open("/foo/bar.txt", { write: true });
* const bytesWritten = await file.write(data); // 11
* ```
*
* @category I/O
*/
write(p: Uint8Array): Promise<number>;
/** Synchronously write the contents of the array buffer (`p`) to the file.
*
* Returns the number of bytes written.
*
* **It is not guaranteed that the full buffer will be written in a single
* call.**
*
* ```ts
* const encoder = new TextEncoder();
* const data = encoder.encode("Hello world");
* using file = Deno.openSync("/foo/bar.txt", { write: true });
* const bytesWritten = file.writeSync(data); // 11
* ```
*/
writeSync(p: Uint8Array): number;
/** Truncates (or extends) the file to reach the specified `len`. If `len`
* is not specified, then the entire file contents are truncated.
*
* ### Truncate the entire file
*
* ```ts
* using file = await Deno.open("my_file.txt", { write: true });
* await file.truncate();
* ```
*
* ### Truncate part of the file
*
* ```ts
* // if "my_file.txt" contains the text "hello world":
* using file = await Deno.open("my_file.txt", { write: true });
* await file.truncate(7);
* const buf = new Uint8Array(100);
* await file.read(buf);
* const text = new TextDecoder().decode(buf); // "hello w"
* ```
*/
truncate(len?: number): Promise<void>;
/** Synchronously truncates (or extends) the file to reach the specified
* `len`. If `len` is not specified, then the entire file contents are
* truncated.
*
* ### Truncate the entire file
*
* ```ts
* using file = Deno.openSync("my_file.txt", { write: true });
* file.truncateSync();
* ```
*
* ### Truncate part of the file
*
* ```ts
* // if "my_file.txt" contains the text "hello world":
* using file = Deno.openSync("my_file.txt", { write: true });
* file.truncateSync(7);
* const buf = new Uint8Array(100);
* file.readSync(buf);
* const text = new TextDecoder().decode(buf); // "hello w"
* ```
*/
truncateSync(len?: number): void;
/** Read the file into an array buffer (`p`).
*
* Resolves to either the number of bytes read during the operation or EOF
* (`null`) if there was nothing more to read.
*
* It is possible for a read to successfully return with `0` bytes. This
* does not indicate EOF.
*
* **It is not guaranteed that the full buffer will be read in a single
* call.**
*
* ```ts
* // if "/foo/bar.txt" contains the text "hello world":
* using file = await Deno.open("/foo/bar.txt");
* const buf = new Uint8Array(100);
* const numberOfBytesRead = await file.read(buf); // 11 bytes
* const text = new TextDecoder().decode(buf); // "hello world"
* ```
*/
read(p: Uint8Array): Promise<number | null>;
/** Synchronously read from the file into an array buffer (`p`).
*
* Returns either the number of bytes read during the operation or EOF
* (`null`) if there was nothing more to read.
*
* It is possible for a read to successfully return with `0` bytes. This
* does not indicate EOF.
*
* **It is not guaranteed that the full buffer will be read in a single
* call.**
*
* ```ts
* // if "/foo/bar.txt" contains the text "hello world":
* using file = Deno.openSync("/foo/bar.txt");
* const buf = new Uint8Array(100);
* const numberOfBytesRead = file.readSync(buf); // 11 bytes
* const text = new TextDecoder().decode(buf); // "hello world"
* ```
*/
readSync(p: Uint8Array): number | null;
/** Seek to the given `offset` under mode given by `whence`. The call
* resolves to the new position within the resource (bytes from the start).
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* using file = await Deno.open(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello world"));
*
* // advance cursor 6 bytes
* const cursorPosition = await file.seek(6, Deno.SeekMode.Start);
* console.log(cursorPosition); // 6
* const buf = new Uint8Array(100);
* await file.read(buf);
* console.log(new TextDecoder().decode(buf)); // "world"
* ```
*
* The seek modes work as follows:
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* const file = await Deno.open(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello world"));
*
* // Seek 6 bytes from the start of the file
* console.log(await file.seek(6, Deno.SeekMode.Start)); // "6"
* // Seek 2 more bytes from the current position
* console.log(await file.seek(2, Deno.SeekMode.Current)); // "8"
* // Seek backwards 2 bytes from the end of the file
* console.log(await file.seek(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
* ```
*/
seek(offset: number | bigint, whence: SeekMode): Promise<number>;
/** Synchronously seek to the given `offset` under mode given by `whence`.
* The new position within the resource (bytes from the start) is returned.
*
* ```ts
* using file = Deno.openSync(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello world"));
*
* // advance cursor 6 bytes
* const cursorPosition = file.seekSync(6, Deno.SeekMode.Start);
* console.log(cursorPosition); // 6
* const buf = new Uint8Array(100);
* file.readSync(buf);
* console.log(new TextDecoder().decode(buf)); // "world"
* ```
*
* The seek modes work as follows:
*
* ```ts
* // Given the file contains "Hello world" text, which is 11 bytes long:
* using file = Deno.openSync(
* "hello.txt",
* { read: true, write: true, truncate: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello world"));
*
* // Seek 6 bytes from the start of the file
* console.log(file.seekSync(6, Deno.SeekMode.Start)); // "6"
* // Seek 2 more bytes from the current position
* console.log(file.seekSync(2, Deno.SeekMode.Current)); // "8"
* // Seek backwards 2 bytes from the end of the file
* console.log(file.seekSync(-2, Deno.SeekMode.End)); // "9" (i.e. 11-2)
* ```
*/
seekSync(offset: number | bigint, whence: SeekMode): number;
/** Resolves to a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
* import { assert } from "jsr:@std/assert";
*
* using file = await Deno.open("hello.txt");
* const fileInfo = await file.stat();
* assert(fileInfo.isFile);
* ```
*/
stat(): Promise<FileInfo>;
/** Synchronously returns a {@linkcode Deno.FileInfo} for the file.
*
* ```ts
* import { assert } from "jsr:@std/assert";
*
* using file = Deno.openSync("hello.txt")
* const fileInfo = file.statSync();
* assert(fileInfo.isFile);
* ```
*/
statSync(): FileInfo;
/**
* Flushes any pending data and metadata operations of the given file
* stream to disk.
*
* ```ts
* const file = await Deno.open(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello World"));
* await file.truncate(1);
* await file.sync();
* console.log(await Deno.readTextFile("my_file.txt")); // H
* ```
*
* @category I/O
*/
sync(): Promise<void>;
/**
* Synchronously flushes any pending data and metadata operations of the given
* file stream to disk.
*
* ```ts
* const file = Deno.openSync(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello World"));
* file.truncateSync(1);
* file.syncSync();
* console.log(Deno.readTextFileSync("my_file.txt")); // H
* ```
*
* @category I/O
*/
syncSync(): void;
/**
* Flushes any pending data operations of the given file stream to disk.
* ```ts
* using file = await Deno.open(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* await file.write(new TextEncoder().encode("Hello World"));
* await file.syncData();
* console.log(await Deno.readTextFile("my_file.txt")); // Hello World
* ```
*
* @category I/O
*/
syncData(): Promise<void>;
/**
* Synchronously flushes any pending data operations of the given file stream
* to disk.
*
* ```ts
* using file = Deno.openSync(
* "my_file.txt",
* { read: true, write: true, create: true },
* );
* file.writeSync(new TextEncoder().encode("Hello World"));
* file.syncDataSync();
* console.log(Deno.readTextFileSync("my_file.txt")); // Hello World
* ```
*
* @category I/O
*/
syncDataSync(): void;
/**
* Changes the access (`atime`) and modification (`mtime`) times of the
* file stream resource. Given times are either in seconds (UNIX epoch
* time) or as `Date` objects.
*
* ```ts
* using file = await Deno.open("file.txt", { create: true, write: true });
* await file.utime(1556495550, new Date());
* ```
*
* @category File System
*/
utime(atime: number | Date, mtime: number | Date): Promise<void>;
/**
* Synchronously changes the access (`atime`) and modification (`mtime`)
* times of the file stream resource. Given times are either in seconds
* (UNIX epoch time) or as `Date` objects.
*
* ```ts
* using file = Deno.openSync("file.txt", { create: true, write: true });
* file.utime(1556495550, new Date());
* ```
*
* @category File System
*/
utimeSync(atime: number | Date, mtime: number | Date): void;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Checks if the file resource is a TTY (terminal).
*
* ```ts
* // This example is system and context specific
* using file = await Deno.open("/dev/tty6");
* file.isTerminal(); // true
* ```
*/
isTerminal(): boolean;
/** **UNSTABLE**: New API, yet to be vetted.
*
* Set TTY to be under raw mode or not. In raw mode, characters are read and
* returned as is, without being processed. All special processing of
* characters by the terminal is disabled, including echoing input
* characters. Reading from a TTY device in raw mode is faster than reading
* from a TTY device in canonical mode.
*
* ```ts
* using file = await Deno.open("/dev/tty6");
* file.setRaw(true, { cbreak: true });
* ```
*/
setRaw(mode: boolean, options?: SetRawOptions): void;
/**
* Acquire an advisory file-system lock for the file.
*
* @param [exclusive=false]
*/
lock(exclusive?: boolean): Promise<void>;
/**
* Synchronously acquire an advisory file-system lock synchronously for the file.
*
* @param [exclusive=false]
*/
lockSync(exclusive?: boolean): void;
/**
* Release an advisory file-system lock for the file.
*/
unlock(): Promise<void>;
/**
* Synchronously release an advisory file-system lock for the file.
*/
unlockSync(): void;
/** Close the file. Closing a file when you are finished with it is
* important to avoid leaking resources.
*
* ```ts
* using file = await Deno.open("my_file.txt");
* // do work with "file" object
* ```
*/
close(): void;
[Symbol.dispose](): void;
}
}
export declare function execute(sql: string, params: unknown[]): Promise<number>;
export declare namespace fs {
const writeFile: typeof Deno_2.writeFile;
const writeTextFile: typeof Deno_2.writeTextFile;
const readTextFile: typeof Deno_2.readTextFile;
const readFile: typeof Deno_2.readFile;
const chmod: typeof Deno_2.chmod;
const chown: typeof Deno_2.chown;
const cwd: typeof Deno_2.cwd;
const makeTempDir: typeof Deno_2.makeTempDir;
const makeTempFile: typeof Deno_2.makeTempFile;
const mkdir: typeof Deno_2.mkdir;
const chdir: typeof Deno_2.chdir;
const copyFile: typeof Deno_2.copyFile;
const readDir: typeof Deno_2.readDir;
const readLink: typeof Deno_2.readLink;
const realPath: typeof Deno_2.realPath;
const remove: typeof Deno_2.remove;
const rename: typeof Deno_2.rename;
const stat: typeof Deno_2.stat;
const lstat: typeof Deno_2.lstat;
const truncate: typeof Deno_2.truncate;
const FsFile: typeof Deno_2.FsFile;
const open: typeof Deno_2.open;
const create: typeof Deno_2.create;
const symlink: typeof Deno_2.symlink;
const link: typeof Deno_2.link;
const utime: typeof Deno_2.utime;
const umask: typeof Deno_2.umask;
}
export declare type HeaderMapType = {
[key: string]: string;
};
export declare function htmlHandler(f: (req: StringRequestType) => MaybeResponse<HtmlResponseType | string>): CallbackType;
export declare type HtmlResponseType = {
headers?: [string, string][];
status?: number;
body: string;
};
export declare class HttpError extends Error {
readonly statusCode: number;
readonly headers: [string, string][] | undefined;
constructor(statusCode: number, message?: string, headers?: [string, string][]);
toString(): string;
toResponse(): ResponseType_2;
}
export declare function jsonHandler(f: (req: JsonRequestType) => MaybeResponse<JsonRequestType | object>): CallbackType;
export declare type JsonRequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: object | string;
};
export declare interface JsonResponseType {
headers?: [string, string][];
status?: number;
body: object;
}
export declare type MaybeResponse<T> = Promise<T | undefined> | T | undefined;
export declare type Method = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT" | "TRACE";
export declare type ParsedPath = {
path: string;
query: URLSearchParams;
};
export declare function parsePath(path: string): ParsedPath;
export declare type PathParamsType = {
[key: string]: string;
};
export declare function query(sql: string, params: unknown[]): Promise<unknown[][]>;
export declare type RequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: Uint8Array;
};
declare type ResponseType_2 = {
headers?: [string, string][];
status?: number;
body?: Uint8Array;
};
export { ResponseType_2 as ResponseType }
export declare enum StatusCodes {
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
EARLY_HINTS = 103,
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
MULTI_STATUS = 207,
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
MOVED_TEMPORARILY = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
PROXY_AUTHENTICATION_REQUIRED = 407,
REQUEST_TIMEOUT = 408,
CONFLICT = 409,
GONE = 410,
LENGTH_REQUIRED = 411,
PRECONDITION_FAILED = 412,
REQUEST_TOO_LONG = 413,
REQUEST_URI_TOO_LONG = 414,
UNSUPPORTED_MEDIA_TYPE = 415,
REQUESTED_RANGE_NOT_SATISFIABLE = 416,
EXPECTATION_FAILED = 417,
IM_A_TEAPOT = 418,
INSUFFICIENT_SPACE_ON_RESOURCE = 419,
METHOD_FAILURE = 420,
MISDIRECTED_REQUEST = 421,
UNPROCESSABLE_ENTITY = 422,
LOCKED = 423,
FAILED_DEPENDENCY = 424,
UPGRADE_REQUIRED = 426,
PRECONDITION_REQUIRED = 428,
TOO_MANY_REQUESTS = 429,
REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
UNAVAILABLE_FOR_LEGAL_REASONS = 451,
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
HTTP_VERSION_NOT_SUPPORTED = 505,
INSUFFICIENT_STORAGE = 507,
NETWORK_AUTHENTICATION_REQUIRED = 511
}
export declare function stringHandler(f: (req: StringRequestType) => MaybeResponse<StringResponseType | string>): CallbackType;
export declare type StringRequestType = {
uri: string;
params: PathParamsType;
headers: HeaderMapType;
user?: UserType;
body?: string;
};
export declare type StringResponseType = {
headers?: [string, string][];
status?: number;
body: string;
};
export declare class Transaction {
finalized: boolean;
constructor();
query(queryStr: string, params: unknown[]): unknown[][];
execute(queryStr: string, params: unknown[]): number;
commit(): void;
rollback(): void;
}
export declare function transaction<T>(f: (tx: Transaction) => T): Promise<T>;
export declare type UserType = {
id: string;
email: string;
csrf: string;
};
export { }

View File

@@ -0,0 +1,463 @@
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var fs;
((fs2) => {
fs2.writeFile = Deno.writeFile;
fs2.writeTextFile = Deno.writeTextFile;
fs2.readTextFile = Deno.readTextFile;
fs2.readFile = Deno.readFile;
fs2.chmod = Deno.chmod;
fs2.chown = Deno.chown;
fs2.cwd = Deno.cwd;
fs2.makeTempDir = Deno.makeTempDir;
fs2.makeTempFile = Deno.makeTempFile;
fs2.mkdir = Deno.mkdir;
fs2.chdir = Deno.chdir;
fs2.copyFile = Deno.copyFile;
fs2.readDir = Deno.readDir;
fs2.readLink = Deno.readLink;
fs2.realPath = Deno.realPath;
fs2.remove = Deno.remove;
fs2.rename = Deno.rename;
fs2.stat = Deno.stat;
fs2.lstat = Deno.lstat;
fs2.truncate = Deno.truncate;
fs2.FsFile = Deno.FsFile;
fs2.open = Deno.open;
fs2.create = Deno.create;
fs2.symlink = Deno.symlink;
fs2.link = Deno.link;
fs2.utime = Deno.utime;
fs2.umask = Deno.umask;
})(fs || (fs = {}));
function decodeFallback(bytes) {
var inputIndex = 0;
var pendingSize = Math.min(256 * 256, bytes.length + 1);
var pending = new Uint16Array(pendingSize);
var chunks = [];
var pendingIndex = 0;
for (; ; ) {
var more = inputIndex < bytes.length;
if (!more || pendingIndex >= pendingSize - 1) {
var subarray = pending.subarray(0, pendingIndex);
var arraylike = subarray;
chunks.push(String.fromCharCode.apply(null, arraylike));
if (!more) {
return chunks.join("");
}
bytes = bytes.subarray(inputIndex);
inputIndex = 0;
pendingIndex = 0;
}
var byte1 = bytes[inputIndex++];
if ((byte1 & 128) === 0) {
pending[pendingIndex++] = byte1;
} else if ((byte1 & 224) === 192) {
var byte2 = bytes[inputIndex++] & 63;
pending[pendingIndex++] = (byte1 & 31) << 6 | byte2;
} else if ((byte1 & 240) === 224) {
var byte2 = bytes[inputIndex++] & 63;
var byte3 = bytes[inputIndex++] & 63;
pending[pendingIndex++] = (byte1 & 31) << 12 | byte2 << 6 | byte3;
} else if ((byte1 & 248) === 240) {
var byte2 = bytes[inputIndex++] & 63;
var byte3 = bytes[inputIndex++] & 63;
var byte4 = bytes[inputIndex++] & 63;
var codepoint = (byte1 & 7) << 18 | byte2 << 12 | byte3 << 6 | byte4;
if (codepoint > 65535) {
codepoint -= 65536;
pending[pendingIndex++] = codepoint >>> 10 & 1023 | 55296;
codepoint = 56320 | codepoint & 1023;
}
pending[pendingIndex++] = codepoint;
} else ;
}
}
function encodeFallback(string) {
var pos = 0;
var len = string.length;
var at = 0;
var tlen = Math.max(32, len + (len >>> 1) + 7);
var target = new Uint8Array(tlen >>> 3 << 3);
while (pos < len) {
var value = string.charCodeAt(pos++);
if (value >= 55296 && value <= 56319) {
if (pos < len) {
var extra = string.charCodeAt(pos);
if ((extra & 64512) === 56320) {
++pos;
value = ((value & 1023) << 10) + (extra & 1023) + 65536;
}
}
if (value >= 55296 && value <= 56319) {
continue;
}
}
if (at + 4 > target.length) {
tlen += 8;
tlen *= 1 + pos / string.length * 2;
tlen = tlen >>> 3 << 3;
var update = new Uint8Array(tlen);
update.set(target);
target = update;
}
if ((value & 4294967168) === 0) {
target[at++] = value;
continue;
} else if ((value & 4294965248) === 0) {
target[at++] = value >>> 6 & 31 | 192;
} else if ((value & 4294901760) === 0) {
target[at++] = value >>> 12 & 15 | 224;
target[at++] = value >>> 6 & 63 | 128;
} else if ((value & 4292870144) === 0) {
target[at++] = value >>> 18 & 7 | 240;
target[at++] = value >>> 12 & 63 | 128;
target[at++] = value >>> 6 & 63 | 128;
} else {
continue;
}
target[at++] = value & 63 | 128;
}
return target.slice ? target.slice(0, at) : target.subarray(0, at);
}
var StatusCodes = /* @__PURE__ */ ((StatusCodes2) => {
StatusCodes2[StatusCodes2["CONTINUE"] = 100] = "CONTINUE";
StatusCodes2[StatusCodes2["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS";
StatusCodes2[StatusCodes2["PROCESSING"] = 102] = "PROCESSING";
StatusCodes2[StatusCodes2["EARLY_HINTS"] = 103] = "EARLY_HINTS";
StatusCodes2[StatusCodes2["OK"] = 200] = "OK";
StatusCodes2[StatusCodes2["CREATED"] = 201] = "CREATED";
StatusCodes2[StatusCodes2["ACCEPTED"] = 202] = "ACCEPTED";
StatusCodes2[StatusCodes2["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION";
StatusCodes2[StatusCodes2["NO_CONTENT"] = 204] = "NO_CONTENT";
StatusCodes2[StatusCodes2["RESET_CONTENT"] = 205] = "RESET_CONTENT";
StatusCodes2[StatusCodes2["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT";
StatusCodes2[StatusCodes2["MULTI_STATUS"] = 207] = "MULTI_STATUS";
StatusCodes2[StatusCodes2["MULTIPLE_CHOICES"] = 300] = "MULTIPLE_CHOICES";
StatusCodes2[StatusCodes2["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY";
StatusCodes2[StatusCodes2["MOVED_TEMPORARILY"] = 302] = "MOVED_TEMPORARILY";
StatusCodes2[StatusCodes2["SEE_OTHER"] = 303] = "SEE_OTHER";
StatusCodes2[StatusCodes2["NOT_MODIFIED"] = 304] = "NOT_MODIFIED";
StatusCodes2[StatusCodes2["USE_PROXY"] = 305] = "USE_PROXY";
StatusCodes2[StatusCodes2["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT";
StatusCodes2[StatusCodes2["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT";
StatusCodes2[StatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
StatusCodes2[StatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
StatusCodes2[StatusCodes2["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
StatusCodes2[StatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN";
StatusCodes2[StatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND";
StatusCodes2[StatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
StatusCodes2[StatusCodes2["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE";
StatusCodes2[StatusCodes2["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED";
StatusCodes2[StatusCodes2["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
StatusCodes2[StatusCodes2["CONFLICT"] = 409] = "CONFLICT";
StatusCodes2[StatusCodes2["GONE"] = 410] = "GONE";
StatusCodes2[StatusCodes2["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED";
StatusCodes2[StatusCodes2["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED";
StatusCodes2[StatusCodes2["REQUEST_TOO_LONG"] = 413] = "REQUEST_TOO_LONG";
StatusCodes2[StatusCodes2["REQUEST_URI_TOO_LONG"] = 414] = "REQUEST_URI_TOO_LONG";
StatusCodes2[StatusCodes2["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
StatusCodes2[StatusCodes2["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE";
StatusCodes2[StatusCodes2["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED";
StatusCodes2[StatusCodes2["IM_A_TEAPOT"] = 418] = "IM_A_TEAPOT";
StatusCodes2[StatusCodes2["INSUFFICIENT_SPACE_ON_RESOURCE"] = 419] = "INSUFFICIENT_SPACE_ON_RESOURCE";
StatusCodes2[StatusCodes2["METHOD_FAILURE"] = 420] = "METHOD_FAILURE";
StatusCodes2[StatusCodes2["MISDIRECTED_REQUEST"] = 421] = "MISDIRECTED_REQUEST";
StatusCodes2[StatusCodes2["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
StatusCodes2[StatusCodes2["LOCKED"] = 423] = "LOCKED";
StatusCodes2[StatusCodes2["FAILED_DEPENDENCY"] = 424] = "FAILED_DEPENDENCY";
StatusCodes2[StatusCodes2["UPGRADE_REQUIRED"] = 426] = "UPGRADE_REQUIRED";
StatusCodes2[StatusCodes2["PRECONDITION_REQUIRED"] = 428] = "PRECONDITION_REQUIRED";
StatusCodes2[StatusCodes2["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
StatusCodes2[StatusCodes2["REQUEST_HEADER_FIELDS_TOO_LARGE"] = 431] = "REQUEST_HEADER_FIELDS_TOO_LARGE";
StatusCodes2[StatusCodes2["UNAVAILABLE_FOR_LEGAL_REASONS"] = 451] = "UNAVAILABLE_FOR_LEGAL_REASONS";
StatusCodes2[StatusCodes2["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
StatusCodes2[StatusCodes2["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
StatusCodes2[StatusCodes2["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
StatusCodes2[StatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
StatusCodes2[StatusCodes2["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
StatusCodes2[StatusCodes2["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED";
StatusCodes2[StatusCodes2["INSUFFICIENT_STORAGE"] = 507] = "INSUFFICIENT_STORAGE";
StatusCodes2[StatusCodes2["NETWORK_AUTHENTICATION_REQUIRED"] = 511] = "NETWORK_AUTHENTICATION_REQUIRED";
return StatusCodes2;
})(StatusCodes || {});
class HttpError extends Error {
constructor(statusCode, message, headers) {
super(message);
__publicField(this, "statusCode");
__publicField(this, "headers");
this.statusCode = statusCode;
this.headers = headers;
}
toString() {
return `HttpError(${this.statusCode}, ${this.message})`;
}
toResponse() {
const m = this.message;
return {
headers: this.headers,
status: this.statusCode,
body: m !== "" ? encodeFallback(m) : void 0
};
}
}
function stringHandler(f) {
return async (req) => {
try {
const body = req.body;
const resp = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body)
});
if (resp === void 0) {
return void 0;
}
if (typeof resp === "string") {
return {
status: 200,
body: encodeFallback(resp)
};
}
const respBody = resp.body;
return {
headers: resp.headers,
status: resp.status,
body: respBody ? encodeFallback(respBody) : void 0
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
status: 500,
body: encodeFallback(`Uncaught error: ${err}`)
};
}
};
}
function htmlHandler(f) {
return async (req) => {
try {
const body = req.body;
const resp = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body)
});
if (resp === void 0) {
return void 0;
}
if (typeof resp === "string") {
return {
headers: [["content-type", "text/html"]],
status: 200,
body: encodeFallback(resp)
};
}
const respBody = resp.body;
return {
headers: [["content-type", "text/html"], ...resp.headers ?? []],
status: resp.status,
body: respBody ? encodeFallback(respBody) : void 0
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
status: 500,
body: encodeFallback(`Uncaught error: ${err}`)
};
}
};
}
function jsonHandler(f) {
return async (req) => {
try {
const body = req.body;
const resp = await f({
uri: req.uri,
params: req.params,
headers: req.headers,
user: req.user,
body: body && decodeFallback(body)
});
if (resp === void 0) {
return void 0;
}
if ("body" in resp) {
const r = resp;
const rBody = r.body;
return {
headers: [["content-type", "application/json"], ...r.headers ?? []],
status: r.status,
body: rBody ? encodeFallback(JSON.stringify(rBody)) : void 0
};
}
return {
headers: [["content-type", "application/json"]],
status: 200,
body: encodeFallback(JSON.stringify(resp))
};
} catch (err) {
if (err instanceof HttpError) {
return err.toResponse();
}
return {
headers: [["content-type", "application/json"]],
status: 500,
body: encodeFallback(`Uncaught error: ${err}`)
};
}
};
}
const routerCallbacks = /* @__PURE__ */ new Map();
function isolateId() {
return rustyscript.functions.isolate_id();
}
function addRoute(method, route, callback) {
if (isolateId() === 0) {
rustyscript.functions.install_route(method, route);
console.debug("JS: Added route:", method, route);
}
routerCallbacks.set(`${method}:${route}`, callback);
}
async function dispatch(method, route, uri, pathParams, headers, user, body) {
const key = `${method}:${route}`;
const cb = routerCallbacks.get(key);
if (!cb) {
throw Error(`Missing callback: ${key}`);
}
return await cb({
uri,
params: Object.fromEntries(pathParams),
headers: Object.fromEntries(headers),
user,
body
}) ?? {
status: 200
/* OK */
};
}
globalThis.__dispatch = dispatch;
const cronCallbacks = /* @__PURE__ */ new Map();
function addCronCallback(name, schedule, cb) {
const cronRegex = /^(@(yearly|monthly|weekly|daily|hourly|))|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*)\s*){6,7})$/;
const matches = cronRegex.test(schedule);
if (!matches) {
throw Error(`Not a valid 6/7-component cron schedule: ${schedule}`);
}
if (isolateId() === 0) {
const id = rustyscript.functions.install_job(name, schedule);
console.debug(`JS: Added cron job (id=${id}): "${name}"`);
cronCallbacks.set(id, cb);
}
}
async function dispatchCron(id) {
const cb = cronCallbacks.get(id);
if (!cb) {
throw Error(`Missing cron callback: ${id}`);
}
try {
await cb();
} catch (err) {
return `${err}`;
}
}
globalThis.__dispatchCron = dispatchCron;
function addPeriodicCallback(milliseconds, cb) {
if (isolateId() !== 0) {
return () => {
};
}
const handle = setInterval(() => {
cb(() => clearInterval(handle));
}, milliseconds);
return () => clearInterval(handle);
}
async function query(sql, params) {
return await rustyscript.async_functions.query(sql, params);
}
async function execute(sql, params) {
return await rustyscript.async_functions.execute(sql, params);
}
class Transaction {
constructor() {
__publicField(this, "finalized");
this.finalized = false;
}
query(queryStr, params) {
return rustyscript.functions.transaction_query(queryStr, params);
}
execute(queryStr, params) {
return rustyscript.functions.transaction_execute(queryStr, params);
}
commit() {
this.finalized = true;
rustyscript.functions.transaction_commit();
}
rollback() {
this.finalized = true;
rustyscript.functions.transaction_rollback();
}
}
async function transaction(f) {
await rustyscript.async_functions.transaction_begin();
const tx = new Transaction();
try {
const r = f(tx);
if (!tx.finalized) {
rustyscript.functions.transaction_rollback();
}
return r;
} catch (e) {
rustyscript.functions.transaction_rollback();
throw e;
}
}
function parsePath(path) {
const queryIndex = path.indexOf("?");
if (queryIndex >= 0) {
return {
path: path.slice(0, queryIndex),
query: new URLSearchParams(path.slice(queryIndex + 1))
};
}
return {
path,
query: new URLSearchParams()
};
}
const _logStderr = function(...args) {
globalThis.Deno.core.print(
`${args.join(" ")}
`,
/* to stderr = */
true
);
};
globalThis.console.log = _logStderr;
globalThis.console.info = _logStderr;
globalThis.console.debug = _logStderr;
export {
HttpError,
StatusCodes,
Transaction,
addCronCallback,
addPeriodicCallback,
addRoute,
execute,
fs,
htmlHandler,
jsonHandler,
parsePath,
query,
stringHandler,
transaction
};

View File

@@ -81,13 +81,20 @@ const columns: ColumnDef<LogJson>[] = [
{ accessorKey: "method" },
{ accessorKey: "url" },
{
accessorKey: "latency_ms",
header: "Latency (ms)",
accessorKey: "latency_ms",
},
{ accessorKey: "client_ip" },
{
accessorKey: "client_cc",
header: "Country Code",
header: "GeoIP",
cell: (ctx) => {
const row = ctx.row.original;
const city = row.client_geoip_city;
if (city) {
return `${city.name} (${city.country_code})`;
}
return row.client_geoip_cc;
},
},
{ accessorKey: "referer" },
{

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GeoipCity = { country_code: string | null, name: string | null, };

View File

@@ -1,7 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { GeoipCity } from "./GeoipCity";
export type LogJson = { id: bigint, created: number, status: number, method: string, url: string, latency_ms: number, client_ip: string,
/**
* Optional two-letter country code.
*/
client_cc: string | null, referer: string, user_agent: string, user_id: string | null, };
client_geoip_cc: string | null, client_geoip_city: GeoipCity | null, referer: string, user_agent: string, user_id: string | null, };

View File

@@ -8,6 +8,7 @@ use log::*;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use trailbase_extension::geoip::{City, DatabaseType};
use trailbase_qs::{Cursor, Order, OrderPrecedent, Query};
use ts_rs::TS;
use uuid::Uuid;
@@ -18,6 +19,21 @@ use crate::constants::{LOGS_RETENTION_DEFAULT, LOGS_TABLE_ID_COLUMN};
use crate::listing::{WhereClause, build_filter_where_clause, cursor_to_value, limit_or_default};
use crate::schema_metadata::{TableMetadata, lookup_and_parse_table_schema};
#[derive(Debug, Deserialize, Serialize, TS)]
pub struct GeoipCity {
country_code: Option<String>,
name: Option<String>,
}
impl From<City> for GeoipCity {
fn from(city: City) -> Self {
return Self {
country_code: city.country_code,
name: city.name,
};
}
}
#[derive(Debug, Serialize, TS)]
pub struct LogJson {
pub id: i64,
@@ -30,7 +46,8 @@ pub struct LogJson {
pub latency_ms: f64,
pub client_ip: String,
/// Optional two-letter country code.
pub client_cc: Option<String>,
pub client_geoip_cc: Option<String>,
pub client_geoip_city: Option<GeoipCity>,
pub referer: String,
pub user_agent: String,
@@ -50,7 +67,9 @@ struct LogEntry {
latency: f64,
client_ip: String,
/// Optional two-letter country code.
client_cc: Option<String>,
client_geoip_cc: Option<String>,
/// Optional city JSON.
client_geoip_city: Option<String>,
referer: String,
user_agent: String,
@@ -82,7 +101,16 @@ impl From<LogEntry> for LogJson {
url: value.url,
latency_ms: value.latency,
client_ip: value.client_ip,
client_cc: value.client_cc,
client_geoip_cc: value.client_geoip_cc,
client_geoip_city: value.client_geoip_city.and_then(|city| {
return serde_json::from_str::<City>(&city)
.map_err(|err| {
log::warn!("Failed to parse geoip city json: {err}");
return err;
})
.map(|city| city.into())
.ok();
}),
referer: value.referer,
user_agent: value.user_agent,
user_id: value.user_id.map(|blob| Uuid::from_bytes(blob).to_string()),
@@ -147,8 +175,10 @@ pub async fn list_logs_handler(
}
let first_page = cursor.is_none();
let geoip_db_type = trailbase_extension::geoip::database_type();
let mut logs = fetch_logs(
conn,
geoip_db_type.clone(),
filter_where_clause.clone(),
cursor,
order.as_ref().unwrap_or_else(|| &DEFAULT_ORDERING),
@@ -162,9 +192,10 @@ pub async fn list_logs_handler(
}
}
let stats = {
let stats = if first_page {
let now = Utc::now();
let args = FetchAggregateArgs {
geoip_db_type,
filter_where_clause: Some(filter_where_clause),
from: now
- Duration::seconds(state.access_config(|c| {
@@ -176,17 +207,14 @@ pub async fn list_logs_handler(
interval: Duration::seconds(600),
};
match first_page {
true => {
let stats = fetch_aggregate_stats(conn, &args).await;
if let Err(ref err) = stats {
warn!("Failed to fetch stats for {args:?}: {err}");
}
stats.ok()
}
false => None,
let stats = fetch_aggregate_stats(conn, &args).await;
if let Err(ref err) = stats {
warn!("Failed to fetch stats for {args:?}: {err}");
}
stats.ok()
} else {
None
};
let response = ListLogsResponse {
@@ -204,6 +232,7 @@ pub async fn list_logs_handler(
async fn fetch_logs(
conn: &trailbase_sqlite::Connection,
geoip_db_type: Option<DatabaseType>,
filter_where_clause: WhereClause,
cursor: Option<Cursor>,
order: &Order,
@@ -238,7 +267,7 @@ async fn fetch_logs(
let sql_query = format!(
r#"
SELECT log.*, geoip_country(log.client_ip) AS client_cc
SELECT log.*, {geoip}
FROM
(SELECT * FROM {LOGS_TABLE_NAME}) AS log
WHERE
@@ -247,6 +276,11 @@ async fn fetch_logs(
{order_clause}
LIMIT :limit
"#,
geoip = match geoip_db_type {
Some(DatabaseType::GeoLite2Country) => "geoip_country(log.client_ip) AS client_geoip_cc",
Some(DatabaseType::GeoLite2City) => "geoip_city_json(log.client_ip) AS client_geoip_city",
_ => "''",
},
);
return Ok(
@@ -266,6 +300,7 @@ pub struct Stats {
#[derive(Debug)]
struct FetchAggregateArgs {
geoip_db_type: Option<DatabaseType>,
filter_where_clause: Option<WhereClause>,
from: DateTime<Utc>,
to: DateTime<Utc>,
@@ -345,7 +380,9 @@ async fn fetch_aggregate_stats(
));
}
if trailbase_extension::geoip::has_geoip_db() {
if args.geoip_db_type == Some(DatabaseType::GeoLite2Country)
|| args.geoip_db_type == Some(DatabaseType::GeoLite2City)
{
lazy_static! {
static ref CC_QUERY: String = format!(
r#"
@@ -439,6 +476,7 @@ mod tests {
}
let args = FetchAggregateArgs {
geoip_db_type: Some(DatabaseType::Unknown),
filter_where_clause: None,
from: from.into(),
to: to.into(),

View File

@@ -14,9 +14,9 @@ static READER: LazyLock<ArcSwap<Option<MaxMindReader>>> =
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct City {
country_code: Option<String>,
name: Option<String>,
subdivisions: Option<Vec<String>>,
pub country_code: Option<String>,
pub name: Option<String>,
pub subdivisions: Option<Vec<String>>,
}
impl City {
@@ -34,6 +34,7 @@ impl City {
pub fn load_geoip_db(path: impl AsRef<Path>) -> Result<(), MaxMindDbError> {
let reader = Reader::open_readfile(path)?;
log::debug!("Loaded geoip DB: {:?}", reader.metadata);
READER.swap(Some(reader).into());
return Ok(());
}
@@ -42,6 +43,27 @@ pub fn has_geoip_db() -> bool {
return READER.load().is_some();
}
#[derive(Clone, Debug, PartialEq)]
pub enum DatabaseType {
Unknown,
GeoLite2Country,
GeoLite2City,
GeoLite2ASN,
}
pub fn database_type() -> Option<DatabaseType> {
if let Some(ref reader) = **READER.load() {
return Some(match reader.metadata.database_type.as_str() {
"GeoLite2-Country" => DatabaseType::GeoLite2Country,
"GeoLite2-City" => DatabaseType::GeoLite2City,
// Autonomous system number.
"GeoLite2-ASN" => DatabaseType::GeoLite2ASN,
_ => DatabaseType::Unknown,
});
}
return None;
}
pub(crate) fn geoip_country(context: &Context) -> Result<Option<String>, Error> {
return geoip_extract(context, |reader, client_ip| {
if let Ok(Some(country)) = reader.lookup::<geoip2::Country>(client_ip) {