refactor(base,learn): people's files kept by halkyon_base
ADR 0043, D2, D4, D6 and D7, the first step towards Todo's attachments: Learn's file store moves into the module, for every server that keeps files, and Learn becomes its first host. halkyon_base gets the store as Learn had it: uploads into the caller's own space by SHA-256, downloads by a row the caller may read, each person's room (FILE_LIMIT_IDENTIFIED_MB, FILE_LIMIT_INSTANCE_MB, the operator's file_allowance, which moves into the module unchanged) and the pruning of files no live row names. It stays free of the sync engine: a host hands it a FileStore saying which of its rows name a file and what it takes (HalkyonBase.files); without one the files endpoint refuses. Its tests, with refusals, run on a stand-in host. Learn hands it learnFiles: its asset rows, read by their space's members, a readable file over 20 MB only as a PDF, up to 100 MB. Its app calls the module; its own asset endpoint stays for released apps alone, forwarding to the module in Learn's own models, whose names are on the wire. Its file tests still pass through it, end to end. Migrations: the module's creates file_allowance. Learn's server migration holds version rows alone, as tables are matched by name (ADR 0014); its device migration creates the module's empty table, as every module's. Todo's, Train's and Meditate's create the empty table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
59fb9c4d6d
commit
b7f0aaaec7
19 changed files with 112084 additions and 348 deletions
|
|
@ -5,7 +5,8 @@
|
|||
/// (ADR 0016), signing in with a password and TOTP and getting back into an
|
||||
/// account (ADR 0017), signing in with SimpleLogin and its forwarding page
|
||||
/// (ADR 0020), the ways in an operator offers, the operator's assistants
|
||||
/// (Halkyon Learn's ADR 0021), and the module's models.
|
||||
/// (Halkyon Learn's ADR 0021), people's files (ADR 0043), and the module's
|
||||
/// models.
|
||||
library;
|
||||
|
||||
export 'src/admission/admission.dart'
|
||||
|
|
@ -35,6 +36,18 @@ export 'src/auth/offered_ways.dart';
|
|||
export 'src/auth/scopes.dart';
|
||||
export 'src/common/network.dart';
|
||||
export 'src/common/short_code.dart';
|
||||
export 'src/files/file_limits.dart'
|
||||
show FileLimits, Room, checkRoom, effectiveLimit, fileLimits, roomOf;
|
||||
export 'src/files/file_pruning.dart'
|
||||
show filePruningGrace, pruneUnreferencedFiles;
|
||||
export 'src/files/file_store.dart';
|
||||
export 'src/files/stored_files.dart'
|
||||
show
|
||||
downloadFile,
|
||||
fileStoreOf,
|
||||
fileUsageOf,
|
||||
finishFileUpload,
|
||||
startFileUpload;
|
||||
export 'src/generated/protocol.dart' hide Protocol;
|
||||
export 'src/halkyon_base.dart';
|
||||
export 'src/instance/data_location.dart';
|
||||
|
|
|
|||
|
|
@ -86,6 +86,13 @@ Future<void> deletePerson(
|
|||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// The room for files the operator granted them (ADR 0043).
|
||||
// DELETE FROM "file_allowance" WHERE "authUserId" = $1
|
||||
await FileAllowance.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(authUserId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// Their password with their keys, authenticators, recovery codes, resets
|
||||
// and sign-ins begun (ADR 0017).
|
||||
// DELETE FROM "account_password" WHERE "authUserId" = $1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
### More room for one person's files than the instance's default (decided
|
||||
### 2026-10-02): the operator grants it on request, by adding a row. Only the
|
||||
### server reads it; deleting the person deletes it.
|
||||
class: FileAllowance
|
||||
table: file_allowance
|
||||
fields:
|
||||
### The person: the AuthUser's id.
|
||||
authUserId: UuidValue
|
||||
### How many bytes of files they may keep.
|
||||
maxBytes: int
|
||||
### Why, for the operator (e.g. "asked by mail, 2026-10-02").
|
||||
note: String?
|
||||
indexes:
|
||||
file_allowance_person_idx:
|
||||
fields: authUserId
|
||||
unique: true
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
### A file doesn't fit: it would take the caller past what they may keep, or the
|
||||
### instance past what it keeps in all. Nothing was stored.
|
||||
exception: FileLimitExceededException
|
||||
fields:
|
||||
### Whether the instance is full, rather than the caller's own room.
|
||||
instanceFull: bool
|
||||
### What the caller may keep, in bytes.
|
||||
limitBytes: int
|
||||
### What the caller keeps now, in bytes.
|
||||
usedBytes: int
|
||||
186
packages/halkyon_base_server/lib/src/files/file_limits.dart
Normal file
186
packages/halkyon_base_server/lib/src/files/file_limits.dart
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_base_server/src/auth/scopes.dart';
|
||||
import 'package:halkyon_base_server/src/files/file_pruning.dart';
|
||||
import 'package:halkyon_base_server/src/files/file_store.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// How much room people's files may take (decided 2026-10-02): a person
|
||||
/// some, and more by a `FileAllowance` the operator grants; and all of them
|
||||
/// together no more than the instance has room for. Each comes from the
|
||||
/// environment, in MiB, with a default: `FILE_LIMIT_IDENTIFIED_MB` (500) and
|
||||
/// `FILE_LIMIT_INSTANCE_MB` (15000; 0 for none). Someone signed in without
|
||||
/// `identified`, an anonymous account left from before (ADR 0020, D25),
|
||||
/// keeps nothing. Every file is in the
|
||||
/// database and in each dump the server keeps: with three dumps, and a
|
||||
/// fourth while a deploy writes its own, files take five times their size on
|
||||
/// the disk.
|
||||
class FileLimits {
|
||||
/// Limits of [identifiedBytes] and [instanceBytes] (null for none).
|
||||
const FileLimits({
|
||||
required this.identifiedBytes,
|
||||
required this.instanceBytes,
|
||||
});
|
||||
|
||||
/// The limits the environment sets, else the defaults.
|
||||
factory FileLimits.fromEnvironment([Map<String, String>? environment]) {
|
||||
final env = environment ?? Platform.environment;
|
||||
int mib(String name, int fallback) =>
|
||||
(int.tryParse(env[name] ?? '') ?? fallback) * _mib;
|
||||
final instance = mib('FILE_LIMIT_INSTANCE_MB', 15000);
|
||||
return FileLimits(
|
||||
identifiedBytes: mib('FILE_LIMIT_IDENTIFIED_MB', 500),
|
||||
instanceBytes: instance > 0 ? instance : null,
|
||||
);
|
||||
}
|
||||
|
||||
/// What an identified person may keep without an allowance, in bytes.
|
||||
final int identifiedBytes;
|
||||
|
||||
/// What everyone together may keep, in bytes; null for no limit.
|
||||
final int? instanceBytes;
|
||||
}
|
||||
|
||||
/// The limits the server works with.
|
||||
FileLimits get fileLimits => _fileLimits ??= FileLimits.fromEnvironment();
|
||||
|
||||
/// Makes [limits] the ones the server works with, for tests; null goes back
|
||||
/// to the environment's.
|
||||
set fileLimits(FileLimits? limits) => _fileLimits = limits;
|
||||
|
||||
FileLimits? _fileLimits;
|
||||
|
||||
const _mib = 1024 * 1024;
|
||||
|
||||
/// How much room the caller has: what their files take, what they may keep,
|
||||
/// and what the instance's files take in all.
|
||||
typedef Room = ({int used, int limit, int instanceUsed});
|
||||
|
||||
/// The room of the caller of [session], whose space is [space]. Uploads not
|
||||
/// finished yet take room too, unless [finishedOnly]: an upload starting
|
||||
/// counts them, or unfinished uploads could fill the disk unnoticed; an
|
||||
/// upload finishing counts only what is kept, itself included.
|
||||
Future<Room> roomOf(
|
||||
Session session,
|
||||
UuidValue space, {
|
||||
bool finishedOnly = false,
|
||||
}) async {
|
||||
final limits = fileLimits;
|
||||
final person = UuidValue.withValidation(
|
||||
session.authenticated!.userIdentifier,
|
||||
);
|
||||
final allowance = await FileAllowance.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.authUserId.equals(person),
|
||||
);
|
||||
return (
|
||||
used: await _spaceBytes(session, space, finishedOnly: finishedOnly),
|
||||
limit:
|
||||
allowance?.maxBytes ??
|
||||
(session.authenticated!.scopes.contains(identified)
|
||||
? limits.identifiedBytes
|
||||
: 0),
|
||||
instanceUsed: await _storedBytes(
|
||||
session,
|
||||
storedFilePathPrefix,
|
||||
finishedOnly: finishedOnly,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Throws a [FileLimitExceededException] unless [room] takes [bytes] more.
|
||||
void checkRoom(Room room, int bytes) {
|
||||
final instance = fileLimits.instanceBytes;
|
||||
final instanceFull = instance != null && room.instanceUsed + bytes > instance;
|
||||
if (instanceFull || room.used + bytes > room.limit) {
|
||||
throw FileLimitExceededException(
|
||||
instanceFull: instanceFull,
|
||||
limitBytes: room.limit,
|
||||
usedBytes: room.used,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// What the caller may keep in all: their limit, or less when the instance
|
||||
/// has less room left.
|
||||
int effectiveLimit(Room room) {
|
||||
final instance = fileLimits.instanceBytes;
|
||||
if (instance == null) {
|
||||
return room.limit;
|
||||
}
|
||||
final left = instance - room.instanceUsed;
|
||||
return room.used + left < room.limit ? room.used + left : room.limit;
|
||||
}
|
||||
|
||||
/// What the stored files of [space] take: those a live row of it names
|
||||
/// (the host's [FileStore.liveHashes]), and those claimed within [filePruningGrace] (an upload's row may
|
||||
/// sync after it), unfinished uploads included unless [finishedOnly]. A
|
||||
/// file no live row names any more, as one a private studyset's rewrite
|
||||
/// replaced (ADR 0019), counts no longer, though pruning keeps it a while.
|
||||
Future<int> _spaceBytes(
|
||||
Session session,
|
||||
UuidValue space, {
|
||||
required bool finishedOnly,
|
||||
}) async {
|
||||
final prefix = '$storedFilePathPrefix${space.uuid}/';
|
||||
final cutoff = clock
|
||||
.now()
|
||||
.toUtc()
|
||||
.subtract(filePruningGrace)
|
||||
.toIso8601String()
|
||||
.replaceFirst('Z', '');
|
||||
// SELECT "path", octet_length("byteData"), "addedTime" >= $3
|
||||
// FROM "serverpod_cloud_storage"
|
||||
// WHERE "storageId" = 'private' AND "path" LIKE $1 [AND "verified"]
|
||||
final rows = await session.db.unsafeQuery(
|
||||
'SELECT "path", octet_length("byteData")::bigint, '
|
||||
'"addedTime" >= @cutoff::timestamp '
|
||||
'FROM "serverpod_cloud_storage" '
|
||||
'WHERE "storageId" = @storage AND "path" LIKE @prefix'
|
||||
'${finishedOnly ? ' AND "verified"' : ''}',
|
||||
parameters: QueryParameters.named({
|
||||
'storage': fileStorage,
|
||||
'prefix': '$prefix%',
|
||||
'cutoff': cutoff,
|
||||
}),
|
||||
);
|
||||
if (rows.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
// The hashes the host's live rows of the space name.
|
||||
final live = await HalkyonBase.of(
|
||||
session.serverpod,
|
||||
).files!.liveHashes(session, space, null);
|
||||
var used = 0;
|
||||
for (final [path as String, bytes as int, recent as bool] in rows) {
|
||||
if (recent || live.contains(path.substring(prefix.length))) {
|
||||
used += bytes;
|
||||
}
|
||||
}
|
||||
return used;
|
||||
}
|
||||
|
||||
/// What the stored files below [prefix] take: unfinished uploads included,
|
||||
/// unless [finishedOnly].
|
||||
Future<int> _storedBytes(
|
||||
Session session,
|
||||
String prefix, {
|
||||
required bool finishedOnly,
|
||||
}) async {
|
||||
// SELECT SUM(octet_length("byteData")) FROM "serverpod_cloud_storage"
|
||||
// WHERE "storageId" = 'private' AND "path" LIKE $1 [AND "verified"]
|
||||
final rows = await session.db.unsafeQuery(
|
||||
'SELECT COALESCE(SUM(octet_length("byteData")), 0)::bigint '
|
||||
'FROM "serverpod_cloud_storage" '
|
||||
'WHERE "storageId" = @storage AND "path" LIKE @prefix'
|
||||
'${finishedOnly ? ' AND "verified"' : ''}',
|
||||
parameters: QueryParameters.named({
|
||||
'storage': fileStorage,
|
||||
'prefix': '$prefix%',
|
||||
}),
|
||||
);
|
||||
return rows.first.first! as int;
|
||||
}
|
||||
90
packages/halkyon_base_server/lib/src/files/file_pruning.dart
Normal file
90
packages/halkyon_base_server/lib/src/files/file_pruning.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import 'package:halkyon_base_server/src/files/file_store.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// How long a stored file stays after it was last claimed (uploaded, or
|
||||
/// asked for again), whether or not a row refers to it: a device
|
||||
/// uploads a file around the time its row syncs, not necessarily after.
|
||||
const filePruningGrace = Duration(days: 7);
|
||||
|
||||
/// Deletes the stored files nobody can reach any more (Halkyon Learn's ADR
|
||||
/// 0004, rule 5; Halkyon's ADR 0043), at most [limit] per call, and returns
|
||||
/// how many: last claimed before [filePruningGrace] ago, and either never
|
||||
/// verified (an upload that wasn't finished) or named by no live row of
|
||||
/// their space, as the host's [FileStore.liveHashes] says (a deleted row is
|
||||
/// a tombstone, which the sync engine hides). Each host schedules it in a
|
||||
/// future call of its own.
|
||||
Future<int> pruneUnreferencedFiles(
|
||||
Session session, {
|
||||
required DateTime now,
|
||||
int limit = 500,
|
||||
}) async {
|
||||
// The columns hold UTC as timestamp without time zone.
|
||||
final timestamp = now
|
||||
.toUtc()
|
||||
.subtract(filePruningGrace)
|
||||
.toIso8601String()
|
||||
.replaceFirst('Z', '');
|
||||
final candidates = await session.db.unsafeQuery(
|
||||
'SELECT "path", "verified" FROM "serverpod_cloud_storage" '
|
||||
'WHERE "storageId" = @storage AND "path" LIKE @prefix '
|
||||
'AND "addedTime" < @cutoff::timestamp '
|
||||
'ORDER BY "addedTime" LIMIT @limit',
|
||||
parameters: QueryParameters.named({
|
||||
'storage': fileStorage,
|
||||
'prefix': '$storedFilePathPrefix%',
|
||||
'cutoff': timestamp,
|
||||
'limit': limit,
|
||||
}),
|
||||
);
|
||||
|
||||
// The candidates' hashes, by space; a path of another form isn't ours to
|
||||
// judge.
|
||||
final hashesBySpace = <UuidValue, Set<String>>{};
|
||||
final unreferenced = <String>{};
|
||||
for (final [path as String, verified as bool] in candidates) {
|
||||
final match = _storedFilePathPattern.firstMatch(path);
|
||||
if (match == null) {
|
||||
continue;
|
||||
}
|
||||
if (!verified) {
|
||||
unreferenced.add(path);
|
||||
} else {
|
||||
(hashesBySpace[UuidValue.withValidation(match[1]!)] ??= {}).add(
|
||||
match[2]!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final store = HalkyonBase.of(session.serverpod).files!;
|
||||
for (final MapEntry(key: space, value: hashes) in hashesBySpace.entries) {
|
||||
final referenced = await store.liveHashes(session, space, hashes);
|
||||
unreferenced.addAll([
|
||||
for (final hash in hashes.difference(referenced))
|
||||
storedFilePath(space, hash),
|
||||
]);
|
||||
}
|
||||
if (unreferenced.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// A file claimed again meanwhile stays. Plain SQL, since the ORM's delete
|
||||
// would read every deleted file to return it.
|
||||
return await session.db.unsafeExecute(
|
||||
'DELETE FROM "serverpod_cloud_storage" '
|
||||
'WHERE "storageId" = @storage AND "path" = ANY(@paths::text[]) '
|
||||
'AND "addedTime" < @cutoff::timestamp',
|
||||
parameters: QueryParameters.named({
|
||||
'storage': fileStorage,
|
||||
'paths': unreferenced.toList(),
|
||||
'cutoff': timestamp,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// A stored file's path: `spaces/<space>/<sha256>`.
|
||||
final _storedFilePathPattern = RegExp(
|
||||
'^${RegExp.escape(storedFilePathPrefix)}'
|
||||
'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/'
|
||||
r'([0-9a-f]{64})$',
|
||||
);
|
||||
80
packages/halkyon_base_server/lib/src/files/file_store.dart
Normal file
80
packages/halkyon_base_server/lib/src/files/file_store.dart
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// A stored file: the space it was uploaded into and its SHA-256.
|
||||
typedef StoredFileRef = ({UuidValue space, String sha256});
|
||||
|
||||
/// The file the synced row [rowId] names, where the caller of [session] may
|
||||
/// read that row (it is live, and in a space they are a member of); null
|
||||
/// otherwise, alike for every reason.
|
||||
typedef FileOfRow = Future<StoredFileRef?> Function(
|
||||
Session session,
|
||||
UuidValue rowId,
|
||||
);
|
||||
|
||||
/// Which of [hashes] (every one, where null) the live rows of [space] name.
|
||||
typedef LiveHashes = Future<Set<String>> Function(
|
||||
Session session,
|
||||
UuidValue space,
|
||||
Set<String>? hashes,
|
||||
);
|
||||
|
||||
/// Whether a file stored complete, [bytes], is kept: [encrypted] where the
|
||||
/// caller said it is sealed, which the module checked by its header
|
||||
/// already.
|
||||
typedef AcceptsFile = bool Function(ByteData bytes, {required bool encrypted});
|
||||
|
||||
/// People's files, as a host keeps them (Halkyon's ADR 0043): in the
|
||||
/// server's private storage, by the uploader's space and the file's
|
||||
/// SHA-256, so a file added twice is stored once. The module knows no
|
||||
/// synced table: the host says which of its rows name a file ([fileOfRow],
|
||||
/// [liveHashes]) and what it keeps ([accepts]). A host without one offers
|
||||
/// no files.
|
||||
final class FileStore {
|
||||
const FileStore({
|
||||
required this.fileOfRow,
|
||||
required this.liveHashes,
|
||||
this.maxBytes = defaultMaxBytes,
|
||||
this.accepts = acceptsAny,
|
||||
});
|
||||
|
||||
/// The largest file a host takes unless it says less: 100 MB (Learn's PDFs,
|
||||
/// since 2026-11-09; Todo's attachments, ADR 0043, D3).
|
||||
static const defaultMaxBytes = 100 * 1024 * 1024;
|
||||
|
||||
/// What sealing adds to a file at most (ADR 0019): its key's ID, the
|
||||
/// nonce, the signature and the cipher's tag, with room to spare.
|
||||
static const encryptionOverhead = 4 * 1024;
|
||||
|
||||
/// The file a row names, for downloads.
|
||||
final FileOfRow fileOfRow;
|
||||
|
||||
/// The hashes a space's live rows name, for the room counted and the
|
||||
/// pruning.
|
||||
final LiveHashes liveHashes;
|
||||
|
||||
/// The largest file taken, before sealing.
|
||||
final int maxBytes;
|
||||
|
||||
/// Whether a file stored complete is kept.
|
||||
final AcceptsFile accepts;
|
||||
|
||||
/// Every file the size and hash allow.
|
||||
static bool acceptsAny(ByteData bytes, {required bool encrypted}) => true;
|
||||
|
||||
/// Sealed files alone (Todo's, whose every row is sealed).
|
||||
static bool acceptsSealedOnly(ByteData bytes, {required bool encrypted}) =>
|
||||
encrypted;
|
||||
}
|
||||
|
||||
/// Where the file with [sha256] of the space [space] is stored.
|
||||
String storedFilePath(UuidValue space, String sha256) =>
|
||||
'$storedFilePathPrefix${space.uuid}/$sha256';
|
||||
|
||||
/// The start of every stored file's path; a space's files are below
|
||||
/// `<prefix><space>/`.
|
||||
const storedFilePathPrefix = 'spaces/';
|
||||
|
||||
/// The storage of every person's files.
|
||||
const fileStorage = 'private';
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
### How much room the caller's files take, and how much they may take.
|
||||
class: FileUsage
|
||||
fields:
|
||||
### What their files take now, in bytes.
|
||||
usedBytes: int
|
||||
### What they may keep, in bytes: their own limit, or less when the instance
|
||||
### has less room left.
|
||||
limitBytes: int
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_base_server/src/files/stored_files.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// People's files (Halkyon's ADR 0043): uploaded into the caller's own
|
||||
/// space by SHA-256, downloaded by the host's row naming them, the room
|
||||
/// they take; see `stored_files.dart`. Only on a host that keeps files
|
||||
/// (`HalkyonBase.files`).
|
||||
class FilesEndpoint extends Endpoint {
|
||||
@override
|
||||
bool get requireLogin => true;
|
||||
|
||||
/// How to upload the caller's file with [sha256] of [byteSize] bytes,
|
||||
/// [encrypted] where sealed; null when the server has it already. Throws
|
||||
/// a [FileLimitExceededException] when it doesn't fit.
|
||||
Future<String?> startUpload(
|
||||
Session session,
|
||||
String sha256,
|
||||
int byteSize, {
|
||||
bool? encrypted,
|
||||
}) => startFileUpload(
|
||||
session,
|
||||
sha256,
|
||||
byteSize,
|
||||
encrypted: encrypted ?? false,
|
||||
);
|
||||
|
||||
/// Completes the upload of the caller's file with [sha256]; whether it is
|
||||
/// stored now.
|
||||
Future<bool> finishUpload(
|
||||
Session session,
|
||||
String sha256, {
|
||||
bool? encrypted,
|
||||
}) => finishFileUpload(session, sha256, encrypted: encrypted ?? false);
|
||||
|
||||
/// How much room the caller's files take, and may take.
|
||||
Future<FileUsage> usage(Session session) => fileUsageOf(session);
|
||||
|
||||
/// The file of the host's row [rowId]. Throws a
|
||||
/// [StoredFileNotFoundException] when it can't be had.
|
||||
Future<ByteData> download(Session session, UuidValue rowId) =>
|
||||
downloadFile(session, rowId);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
### The file asked for can't be had: no row of that ID the caller may read, a
|
||||
### deleted row, or a file not uploaded yet. Said alike for each, so a caller
|
||||
### learns nothing of rows they can't read.
|
||||
exception: StoredFileNotFoundException
|
||||
fields:
|
||||
### Always 'file'.
|
||||
what: String
|
||||
160
packages/halkyon_base_server/lib/src/files/stored_files.dart
Normal file
160
packages/halkyon_base_server/lib/src/files/stored_files.dart
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:halkyon_base_server/src/files/file_limits.dart';
|
||||
import 'package:halkyon_base_server/src/files/file_store.dart';
|
||||
import 'package:halkyon_base_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_base_server/src/halkyon_base.dart';
|
||||
import 'package:serverpod/protocol.dart' show CloudStorageEntry;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
/// People's files (Halkyon's ADR 0043, from Halkyon Learn's ADR 0004, rule
|
||||
/// 5): what the module's `files` endpoint does, and a host's own endpoint
|
||||
/// may (Learn's `asset`, kept for its released apps, D7).
|
||||
///
|
||||
/// Uploading goes into the caller's own space, by the file's hash, before or
|
||||
/// after the row naming it syncs: the server asks for the file only when it
|
||||
/// lacks it, and keeps it only when its content has that hash and the host
|
||||
/// accepts it ([FileStore.accepts]). Downloading goes by the host's row
|
||||
/// ([FileStore.fileOfRow]); any other asks as if the file weren't there.
|
||||
/// Files no live row names any more are pruned (`pruneUnreferencedFiles`).
|
||||
|
||||
/// How to upload the caller's file with [sha256] (lowercase hex) of
|
||||
/// [byteSize] bytes, [encrypted] where it is sealed (ADR 0019), which may be
|
||||
/// larger by [FileStore.encryptionOverhead]; null when the server has it
|
||||
/// already. Throws a [FileLimitExceededException] when it doesn't fit (see
|
||||
/// `FileLimits`).
|
||||
Future<String?> startFileUpload(
|
||||
Session session,
|
||||
String sha256,
|
||||
int byteSize, {
|
||||
bool encrypted = false,
|
||||
}) async {
|
||||
final store = fileStoreOf(session);
|
||||
_checkHash(sha256);
|
||||
final most = store.maxBytes + (encrypted ? FileStore.encryptionOverhead : 0);
|
||||
if (byteSize <= 0 || byteSize > most) {
|
||||
throw ArgumentError.value(byteSize, 'byteSize', 'is out of range');
|
||||
}
|
||||
final space = _caller(session);
|
||||
final path = storedFilePath(space, sha256);
|
||||
if (await session.storage.fileExists(storageId: fileStorage, path: path)) {
|
||||
// The file is claimed anew, so pruning leaves it alone for a while even
|
||||
// if the row that claims it hasn't synced yet.
|
||||
// UPDATE "serverpod_cloud_storage" SET "addedTime" = $1
|
||||
// WHERE "storageId" = 'private' AND "path" = $2
|
||||
await CloudStorageEntry.db.updateWhere(
|
||||
session,
|
||||
columnValues: (t) => [t.addedTime(clock.now().toUtc())],
|
||||
where: (t) => t.storageId.equals(fileStorage) & t.path.equals(path),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
checkRoom(await roomOf(session, space), byteSize);
|
||||
return await session.storage.createUploadDescription(
|
||||
storageId: fileStorage,
|
||||
path: path,
|
||||
options: UploadOptions(contentLength: byteSize, maxFileSize: most),
|
||||
);
|
||||
}
|
||||
|
||||
/// Completes the upload of the caller's file with [sha256]; whether it is
|
||||
/// stored now. A file whose content has another hash is deleted, and so is
|
||||
/// one the host doesn't accept, and one that no longer fits, with a
|
||||
/// [FileLimitExceededException]. An [encrypted] file (ADR 0019) can't be
|
||||
/// looked into: it must start as a sealed file does (`HLKF`, version 1), or
|
||||
/// it is deleted, and its size alone counts, as the quotas' sizes do.
|
||||
Future<bool> finishFileUpload(
|
||||
Session session,
|
||||
String sha256, {
|
||||
bool encrypted = false,
|
||||
}) async {
|
||||
final store = fileStoreOf(session);
|
||||
_checkHash(sha256);
|
||||
final path = storedFilePath(_caller(session), sha256);
|
||||
if (!await session.storage.verifyUpload(
|
||||
storageId: fileStorage,
|
||||
path: path,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
final bytes = await session.storage.retrieveFile(
|
||||
storageId: fileStorage,
|
||||
path: path,
|
||||
);
|
||||
if (_hashOf(bytes) != sha256 ||
|
||||
(encrypted && !_isSealed(bytes)) ||
|
||||
bytes.lengthInBytes > store.maxBytes + FileStore.encryptionOverhead ||
|
||||
(!encrypted && bytes.lengthInBytes > store.maxBytes) ||
|
||||
!store.accepts(bytes, encrypted: encrypted)) {
|
||||
await session.storage.deleteFile(storageId: fileStorage, path: path);
|
||||
return false;
|
||||
}
|
||||
// Checked again now that it is stored: uploads started side by side each
|
||||
// fitted on their own.
|
||||
try {
|
||||
checkRoom(await roomOf(session, _caller(session), finishedOnly: true), 0);
|
||||
} on FileLimitExceededException {
|
||||
await session.storage.deleteFile(storageId: fileStorage, path: path);
|
||||
rethrow;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// How much room the caller's files take, and may take.
|
||||
Future<FileUsage> fileUsageOf(Session session) async {
|
||||
fileStoreOf(session);
|
||||
final room = await roomOf(session, _caller(session));
|
||||
return FileUsage(usedBytes: room.used, limitBytes: effectiveLimit(room));
|
||||
}
|
||||
|
||||
/// The file the host's row [rowId] names. Throws a
|
||||
/// [StoredFileNotFoundException] when the caller may not read that row, it
|
||||
/// names no file, or its file isn't uploaded yet.
|
||||
Future<ByteData> downloadFile(Session session, UuidValue rowId) async {
|
||||
final file = await fileStoreOf(session).fileOfRow(session, rowId);
|
||||
if (file == null || !_isHash(file.sha256)) {
|
||||
throw StoredFileNotFoundException(what: 'file');
|
||||
}
|
||||
try {
|
||||
return await session.storage.retrieveFile(
|
||||
storageId: fileStorage,
|
||||
path: storedFilePath(file.space, file.sha256),
|
||||
);
|
||||
} on CloudStorageFileNotFoundException {
|
||||
throw StoredFileNotFoundException(what: 'file');
|
||||
}
|
||||
}
|
||||
|
||||
/// The host's file store; a host without one offers no files, and an app
|
||||
/// asking anyway is refused.
|
||||
FileStore fileStoreOf(Session session) =>
|
||||
HalkyonBase.of(session.serverpod).files ??
|
||||
(throw StateError('This server keeps no files.'));
|
||||
|
||||
UuidValue _caller(Session session) =>
|
||||
UuidValue.withValidation(session.authenticated!.userIdentifier);
|
||||
|
||||
bool _isHash(String value) => RegExp(r'^[0-9a-f]{64}$').hasMatch(value);
|
||||
|
||||
void _checkHash(String sha256) {
|
||||
if (!_isHash(sha256)) {
|
||||
throw ArgumentError.value(sha256, 'sha256', 'is no SHA-256 in hex');
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether [bytes] are a sealed file's (ADR 0019): they start as the apps
|
||||
/// seal every file, `HLKF` and its format's version, 1. What follows can't
|
||||
/// be looked into, but a readable file said to be sealed can't skip the
|
||||
/// host's checks unnoticed.
|
||||
bool _isSealed(ByteData bytes) {
|
||||
const magic = 'HLKF';
|
||||
return bytes.lengthInBytes > magic.length + 1 &&
|
||||
String.fromCharCodes(Uint8List.sublistView(bytes, 0, magic.length)) ==
|
||||
magic &&
|
||||
bytes.getUint8(magic.length) == 1;
|
||||
}
|
||||
|
||||
String _hashOf(ByteData bytes) =>
|
||||
sha256.convert(Uint8List.sublistView(bytes)).toString();
|
||||
|
|
@ -48,6 +48,7 @@ import '../assistant/assistant_endpoint.dart' as _i1zejsgk;
|
|||
import '../assistant/assistant_operator_endpoint.dart' as _ijs35jr5;
|
||||
import '../auth/account_endpoint.dart' as _ii6ycf6v;
|
||||
import '../auth/email_code_endpoint.dart' as _in3cimlf;
|
||||
import '../files/files_endpoint.dart' as _idx8vriz;
|
||||
import '../instance/instance_endpoint.dart' as _i3g8bfc2;
|
||||
import '../keys/keys_endpoint.dart' as _icowoccn;
|
||||
import '../operator/instance_operator_endpoint.dart' as _iyf6lavu;
|
||||
|
|
@ -92,6 +93,12 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
'emailCode',
|
||||
'halkyon_base',
|
||||
),
|
||||
'files': _idx8vriz.FilesEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
'files',
|
||||
'halkyon_base',
|
||||
),
|
||||
'instance': _i3g8bfc2.InstanceEndpoint()
|
||||
..initialize(
|
||||
server,
|
||||
|
|
@ -544,6 +551,98 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
),
|
||||
},
|
||||
);
|
||||
connectors['files'] = _is.EndpointConnector(
|
||||
name: 'files',
|
||||
endpoint: endpoints['files']!,
|
||||
methodConnectors: {
|
||||
'startUpload': _is.MethodConnector(
|
||||
name: 'startUpload',
|
||||
params: {
|
||||
'sha256': _is.ParameterDescription(
|
||||
name: 'sha256',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'byteSize': _is.ParameterDescription(
|
||||
name: 'byteSize',
|
||||
type: _is.getType<int>(),
|
||||
nullable: false,
|
||||
),
|
||||
'encrypted': _is.ParameterDescription(
|
||||
name: 'encrypted',
|
||||
type: _is.getType<bool?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['files'] as _idx8vriz.FilesEndpoint).startUpload(
|
||||
session,
|
||||
params['sha256'],
|
||||
params['byteSize'],
|
||||
encrypted: params['encrypted'],
|
||||
),
|
||||
),
|
||||
'finishUpload': _is.MethodConnector(
|
||||
name: 'finishUpload',
|
||||
params: {
|
||||
'sha256': _is.ParameterDescription(
|
||||
name: 'sha256',
|
||||
type: _is.getType<String>(),
|
||||
nullable: false,
|
||||
),
|
||||
'encrypted': _is.ParameterDescription(
|
||||
name: 'encrypted',
|
||||
type: _is.getType<bool?>(),
|
||||
nullable: true,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['files'] as _idx8vriz.FilesEndpoint).finishUpload(
|
||||
session,
|
||||
params['sha256'],
|
||||
encrypted: params['encrypted'],
|
||||
),
|
||||
),
|
||||
'usage': _is.MethodConnector(
|
||||
name: 'usage',
|
||||
params: {},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async => (endpoints['files'] as _idx8vriz.FilesEndpoint).usage(
|
||||
session,
|
||||
),
|
||||
),
|
||||
'download': _is.MethodConnector(
|
||||
name: 'download',
|
||||
params: {
|
||||
'rowId': _is.ParameterDescription(
|
||||
name: 'rowId',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['files'] as _idx8vriz.FilesEndpoint).download(
|
||||
session,
|
||||
params['rowId'],
|
||||
),
|
||||
),
|
||||
},
|
||||
);
|
||||
connectors['instance'] = _is.EndpointConnector(
|
||||
name: 'instance',
|
||||
endpoint: endpoints['instance']!,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,638 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// More room for one person's files than the instance's default (decided
|
||||
/// 2026-10-02): the operator grants it on request, by adding a row. Only the
|
||||
/// server reads it; deleting the person deletes it.
|
||||
abstract class FileAllowance
|
||||
implements _is.TableRow<int?>, _is.ProtocolSerialization {
|
||||
FileAllowance._({
|
||||
this.id,
|
||||
required this.authUserId,
|
||||
required this.maxBytes,
|
||||
this.note,
|
||||
});
|
||||
|
||||
factory FileAllowance({
|
||||
int? id,
|
||||
required _is.UuidValue authUserId,
|
||||
required int maxBytes,
|
||||
String? note,
|
||||
}) = _FileAllowanceImpl;
|
||||
|
||||
factory FileAllowance.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return FileAllowance(
|
||||
id: jsonSerialization['id'] as int?,
|
||||
authUserId: _is.UuidValueJsonExtension.fromJson(
|
||||
jsonSerialization['authUserId'],
|
||||
),
|
||||
maxBytes: jsonSerialization['maxBytes'] as int,
|
||||
note: jsonSerialization['note'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
static final t = FileAllowanceTable();
|
||||
|
||||
static const db = FileAllowanceRepository._();
|
||||
|
||||
@override
|
||||
int? id;
|
||||
|
||||
/// The person: the AuthUser's id.
|
||||
_is.UuidValue authUserId;
|
||||
|
||||
/// How many bytes of files they may keep.
|
||||
int maxBytes;
|
||||
|
||||
/// Why, for the operator (e.g. "asked by mail, 2026-10-02").
|
||||
String? note;
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => t;
|
||||
|
||||
/// Returns a shallow copy of this [FileAllowance]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
FileAllowance copyWith({
|
||||
int? id,
|
||||
_is.UuidValue? authUserId,
|
||||
int? maxBytes,
|
||||
String? note,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.FileAllowance',
|
||||
if (id != null) 'id': id,
|
||||
'authUserId': authUserId.toJson(),
|
||||
'maxBytes': maxBytes,
|
||||
if (note != null) 'note': note,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.FileAllowance',
|
||||
if (id != null) 'id': id,
|
||||
'authUserId': authUserId.toJson(),
|
||||
'maxBytes': maxBytes,
|
||||
if (note != null) 'note': note,
|
||||
};
|
||||
}
|
||||
|
||||
static FileAllowanceInclude include() {
|
||||
return FileAllowanceInclude._();
|
||||
}
|
||||
|
||||
static FileAllowanceIncludeList includeList({
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<FileAllowanceTable>? orderBy,
|
||||
_is.OrderByListBuilder<FileAllowanceTable>? orderByList,
|
||||
FileAllowanceInclude? include,
|
||||
}) {
|
||||
return FileAllowanceIncludeList._(
|
||||
where: where,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(FileAllowance.t),
|
||||
orderByList: orderByList?.call(FileAllowance.t),
|
||||
include: include,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _Undefined {}
|
||||
|
||||
class _FileAllowanceImpl extends FileAllowance {
|
||||
_FileAllowanceImpl({
|
||||
int? id,
|
||||
required _is.UuidValue authUserId,
|
||||
required int maxBytes,
|
||||
String? note,
|
||||
}) : super._(
|
||||
id: id,
|
||||
authUserId: authUserId,
|
||||
maxBytes: maxBytes,
|
||||
note: note,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [FileAllowance]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
FileAllowance copyWith({
|
||||
Object? id = _Undefined,
|
||||
_is.UuidValue? authUserId,
|
||||
int? maxBytes,
|
||||
Object? note = _Undefined,
|
||||
}) {
|
||||
return FileAllowance(
|
||||
id: id is int? ? id : this.id,
|
||||
authUserId: authUserId ?? this.authUserId,
|
||||
maxBytes: maxBytes ?? this.maxBytes,
|
||||
note: note is String? ? note : this.note,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FileAllowanceUpdateTable extends _is.UpdateTable<FileAllowanceTable> {
|
||||
FileAllowanceUpdateTable(super.table);
|
||||
|
||||
_is.ColumnValue<_is.UuidValue, _is.UuidValue> authUserId(
|
||||
_is.UuidValue value,
|
||||
) => _is.ColumnValue(
|
||||
table.authUserId,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<int, int> maxBytes(int value) => _is.ColumnValue(
|
||||
table.maxBytes,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<String, String> note(String? value) => _is.ColumnValue(
|
||||
table.note,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class FileAllowanceTable extends _is.Table<int?> {
|
||||
FileAllowanceTable({super.tableRelation})
|
||||
: super(tableName: 'file_allowance') {
|
||||
updateTable = FileAllowanceUpdateTable(this);
|
||||
authUserId = _is.ColumnUuid(
|
||||
'authUserId',
|
||||
this,
|
||||
);
|
||||
maxBytes = _is.ColumnInt(
|
||||
'maxBytes',
|
||||
this,
|
||||
);
|
||||
note = _is.ColumnString(
|
||||
'note',
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
late final FileAllowanceUpdateTable updateTable;
|
||||
|
||||
/// The person: the AuthUser's id.
|
||||
late final _is.ColumnUuid authUserId;
|
||||
|
||||
/// How many bytes of files they may keep.
|
||||
late final _is.ColumnInt maxBytes;
|
||||
|
||||
/// Why, for the operator (e.g. "asked by mail, 2026-10-02").
|
||||
late final _is.ColumnString note;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
authUserId,
|
||||
maxBytes,
|
||||
note,
|
||||
];
|
||||
}
|
||||
|
||||
class FileAllowanceInclude extends _is.IncludeObject {
|
||||
FileAllowanceInclude._();
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => FileAllowance.t;
|
||||
}
|
||||
|
||||
class FileAllowanceIncludeList extends _is.IncludeList {
|
||||
FileAllowanceIncludeList._({
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? where,
|
||||
super.limit,
|
||||
super.offset,
|
||||
super.orderBy,
|
||||
super.orderByList,
|
||||
super.include,
|
||||
}) {
|
||||
super.where = where?.call(FileAllowance.t);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, _is.Include?> get includes => include?.includes ?? {};
|
||||
|
||||
@override
|
||||
_is.Table<int?> get table => FileAllowance.t;
|
||||
}
|
||||
|
||||
class FileAllowanceRepository {
|
||||
const FileAllowanceRepository._();
|
||||
|
||||
/// Returns a list of [FileAllowance]s matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order of the items use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// The maximum number of items can be set by [limit]. If no limit is set,
|
||||
/// all items matching the query will be returned.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which [limit] (or all)
|
||||
/// items are read from the database.
|
||||
///
|
||||
/// ```dart
|
||||
/// var persons = await Persons.db.find(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.firstName,
|
||||
/// limit: 100,
|
||||
/// );
|
||||
/// ```
|
||||
Future<List<FileAllowance>> find(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<FileAllowanceTable>? orderBy,
|
||||
_is.OrderByListBuilder<FileAllowanceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.find<FileAllowance>(
|
||||
where: where?.call(FileAllowance.t),
|
||||
orderBy: orderBy?.call(FileAllowance.t),
|
||||
orderByList: orderByList?.call(FileAllowance.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the first matching [FileAllowance] matching the given query parameters.
|
||||
///
|
||||
/// Use [where] to specify which items to include in the return value.
|
||||
/// If none is specified, all items will be returned.
|
||||
///
|
||||
/// To specify the order use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// [offset] defines how many items to skip, after which the next one will be picked.
|
||||
///
|
||||
/// ```dart
|
||||
/// var youngestPerson = await Persons.db.findFirstRow(
|
||||
/// session,
|
||||
/// where: (t) => t.lastName.equals('Jones'),
|
||||
/// orderBy: (t) => t.age,
|
||||
/// );
|
||||
/// ```
|
||||
Future<FileAllowance?> findFirstRow(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? where,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<FileAllowanceTable>? orderBy,
|
||||
_is.OrderByListBuilder<FileAllowanceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findFirstRow<FileAllowance>(
|
||||
where: where?.call(FileAllowance.t),
|
||||
orderBy: orderBy?.call(FileAllowance.t),
|
||||
orderByList: orderByList?.call(FileAllowance.t),
|
||||
offset: offset,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds a single [FileAllowance] by its [id] or null if no such row exists.
|
||||
Future<FileAllowance?> findById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
_is.Transaction? transaction,
|
||||
_is.LockMode? lockMode,
|
||||
_is.LockBehavior? lockBehavior,
|
||||
}) async {
|
||||
return session.db.findById<FileAllowance>(
|
||||
id,
|
||||
transaction: transaction,
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts all [FileAllowance]s in the list and returns the inserted rows.
|
||||
///
|
||||
/// The returned [FileAllowance]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// insert, none of the rows will be inserted.
|
||||
///
|
||||
/// If [ignoreConflicts] is set to `true`, rows that conflict with existing
|
||||
/// rows are silently skipped, and only the successfully inserted rows are
|
||||
/// returned.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the inserted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<FileAllowance>> insert(
|
||||
_is.DatabaseSession session,
|
||||
List<FileAllowance> rows, {
|
||||
_is.Transaction? transaction,
|
||||
bool ignoreConflicts = false,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.insert<FileAllowance>(
|
||||
rows,
|
||||
transaction: transaction,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts a single [FileAllowance] and returns the inserted row.
|
||||
///
|
||||
/// The returned [FileAllowance] will have its `id` field set.
|
||||
Future<FileAllowance> insertRow(
|
||||
_is.DatabaseSession session,
|
||||
FileAllowance row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.insertRow<FileAllowance>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts all [FileAllowance]s in the list and returns the resulting rows.
|
||||
///
|
||||
/// If a row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated with the new values. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies to rows matching the
|
||||
/// given expression. Conflicting rows that don't match are skipped and not
|
||||
/// returned, so the resulting list may be shorter than [rows].
|
||||
///
|
||||
/// The returned [FileAllowance]s will have their `id` fields set.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fails,
|
||||
/// none of the rows will be affected.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the resulting rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<FileAllowance>> upsert(
|
||||
_is.DatabaseSession session,
|
||||
List<FileAllowance> rows, {
|
||||
required _is.ColumnSelections<FileAllowanceTable> conflictColumns,
|
||||
_is.ColumnSelections<FileAllowanceTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.upsert<FileAllowance>(
|
||||
rows,
|
||||
conflictColumns: conflictColumns(FileAllowance.t),
|
||||
updateColumns: updateColumns?.call(FileAllowance.t),
|
||||
updateWhere: updateWhere?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Upserts a single [FileAllowance] and returns the resulting row.
|
||||
///
|
||||
/// If the row conflicts on the given [conflictColumns], the existing row is
|
||||
/// updated. Otherwise, a new row is inserted.
|
||||
///
|
||||
/// If [updateColumns] is provided, only those columns will be updated on
|
||||
/// conflict. If null, all non-conflict, non-id columns are updated.
|
||||
///
|
||||
/// If [updateWhere] is provided, the update only applies when the existing
|
||||
/// row matches the expression. Returns `null` if no row was affected — for
|
||||
/// example when [updateWhere] does not match the conflicting row.
|
||||
///
|
||||
/// The returned [FileAllowance] will have its `id` field set.
|
||||
Future<FileAllowance?> upsertRow(
|
||||
_is.DatabaseSession session,
|
||||
FileAllowance row, {
|
||||
required _is.ColumnSelections<FileAllowanceTable> conflictColumns,
|
||||
_is.ColumnSelections<FileAllowanceTable>? updateColumns,
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? updateWhere,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.upsertRow<FileAllowance>(
|
||||
row,
|
||||
conflictColumns: conflictColumns(FileAllowance.t),
|
||||
updateColumns: updateColumns?.call(FileAllowance.t),
|
||||
updateWhere: updateWhere?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [FileAllowance]s in the list and returns the updated rows. If
|
||||
/// [columns] is provided, only those columns will be updated. Defaults to
|
||||
/// all columns.
|
||||
/// This is an atomic operation, meaning that if one of the rows fails to
|
||||
/// update, none of the rows will be updated.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<FileAllowance>> update(
|
||||
_is.DatabaseSession session,
|
||||
List<FileAllowance> rows, {
|
||||
_is.ColumnSelections<FileAllowanceTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.update<FileAllowance>(
|
||||
rows,
|
||||
columns: columns?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [FileAllowance]. The row needs to have its id set.
|
||||
/// Optionally, a list of [columns] can be provided to only update those
|
||||
/// columns. Defaults to all columns.
|
||||
Future<FileAllowance> updateRow(
|
||||
_is.DatabaseSession session,
|
||||
FileAllowance row, {
|
||||
_is.ColumnSelections<FileAllowanceTable>? columns,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateRow<FileAllowance>(
|
||||
row,
|
||||
columns: columns?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates a single [FileAllowance] by its [id] with the specified [columnValues].
|
||||
/// Returns the updated row or null if no row with the given id exists.
|
||||
Future<FileAllowance?> updateById(
|
||||
_is.DatabaseSession session,
|
||||
int id, {
|
||||
required _is.ColumnValueListBuilder<FileAllowanceUpdateTable> columnValues,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.updateById<FileAllowance>(
|
||||
id,
|
||||
columnValues: columnValues(FileAllowance.t.updateTable),
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Updates all [FileAllowance]s matching the [where] expression with the specified [columnValues].
|
||||
/// Returns the list of updated rows.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the updated rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<FileAllowance>> updateWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.ColumnValueListBuilder<FileAllowanceUpdateTable> columnValues,
|
||||
required _is.WhereExpressionBuilder<FileAllowanceTable> where,
|
||||
int? limit,
|
||||
int? offset,
|
||||
_is.OrderByBuilder<FileAllowanceTable>? orderBy,
|
||||
_is.OrderByListBuilder<FileAllowanceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.updateWhere<FileAllowance>(
|
||||
columnValues: columnValues(FileAllowance.t.updateTable),
|
||||
where: where(FileAllowance.t),
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
orderBy: orderBy?.call(FileAllowance.t),
|
||||
orderByList: orderByList?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all [FileAllowance]s in the list and returns the deleted rows.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// This is an atomic operation, meaning that if one of the rows fail to
|
||||
/// be deleted, none of the rows will be deleted.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<FileAllowance>> delete(
|
||||
_is.DatabaseSession session,
|
||||
List<FileAllowance> rows, {
|
||||
_is.OrderByBuilder<FileAllowanceTable>? orderBy,
|
||||
_is.OrderByListBuilder<FileAllowanceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.delete<FileAllowance>(
|
||||
rows,
|
||||
orderBy: orderBy?.call(FileAllowance.t),
|
||||
orderByList: orderByList?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes a single [FileAllowance].
|
||||
Future<FileAllowance> deleteRow(
|
||||
_is.DatabaseSession session,
|
||||
FileAllowance row, {
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.deleteRow<FileAllowance>(
|
||||
row,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes all rows matching the [where] expression.
|
||||
///
|
||||
/// To specify the order of the returned rows use [orderBy] or [orderByList]
|
||||
/// when sorting by multiple columns.
|
||||
///
|
||||
/// If [noReturn] is set to `true`, the deleted rows are not read back from
|
||||
/// the database and an empty list is returned. This avoids the overhead of
|
||||
/// transferring and deserializing the rows when the result is not needed.
|
||||
Future<List<FileAllowance>> deleteWhere(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<FileAllowanceTable> where,
|
||||
_is.OrderByBuilder<FileAllowanceTable>? orderBy,
|
||||
_is.OrderByListBuilder<FileAllowanceTable>? orderByList,
|
||||
_is.Transaction? transaction,
|
||||
bool noReturn = false,
|
||||
}) async {
|
||||
return session.db.deleteWhere<FileAllowance>(
|
||||
where: where(FileAllowance.t),
|
||||
orderBy: orderBy?.call(FileAllowance.t),
|
||||
orderByList: orderByList?.call(FileAllowance.t),
|
||||
transaction: transaction,
|
||||
noReturn: noReturn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Counts the number of rows matching the [where] expression. If omitted,
|
||||
/// will return the count of all rows in the table.
|
||||
Future<int> count(
|
||||
_is.DatabaseSession session, {
|
||||
_is.WhereExpressionBuilder<FileAllowanceTable>? where,
|
||||
int? limit,
|
||||
_is.Transaction? transaction,
|
||||
}) async {
|
||||
return session.db.count<FileAllowance>(
|
||||
where: where?.call(FileAllowance.t),
|
||||
limit: limit,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
|
||||
/// Acquires row-level locks on [FileAllowance] rows matching the [where] expression.
|
||||
Future<void> lockRows(
|
||||
_is.DatabaseSession session, {
|
||||
required _is.WhereExpressionBuilder<FileAllowanceTable> where,
|
||||
required _is.LockMode lockMode,
|
||||
required _is.Transaction transaction,
|
||||
_is.LockBehavior lockBehavior = _is.LockBehavior.wait,
|
||||
}) async {
|
||||
return session.db.lockRows<FileAllowance>(
|
||||
where: where(FileAllowance.t),
|
||||
lockMode: lockMode,
|
||||
lockBehavior: lockBehavior,
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// A file doesn't fit: it would take the caller past what they may keep, or the
|
||||
/// instance past what it keeps in all. Nothing was stored.
|
||||
abstract class FileLimitExceededException
|
||||
implements
|
||||
_is.SerializableException,
|
||||
_is.SerializableModel,
|
||||
_is.ProtocolSerialization {
|
||||
FileLimitExceededException._({
|
||||
required this.instanceFull,
|
||||
required this.limitBytes,
|
||||
required this.usedBytes,
|
||||
});
|
||||
|
||||
factory FileLimitExceededException({
|
||||
required bool instanceFull,
|
||||
required int limitBytes,
|
||||
required int usedBytes,
|
||||
}) = _FileLimitExceededExceptionImpl;
|
||||
|
||||
factory FileLimitExceededException.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return FileLimitExceededException(
|
||||
instanceFull: _is.BoolJsonExtension.fromJson(
|
||||
jsonSerialization['instanceFull'],
|
||||
),
|
||||
limitBytes: jsonSerialization['limitBytes'] as int,
|
||||
usedBytes: jsonSerialization['usedBytes'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether the instance is full, rather than the caller's own room.
|
||||
bool instanceFull;
|
||||
|
||||
/// What the caller may keep, in bytes.
|
||||
int limitBytes;
|
||||
|
||||
/// What the caller keeps now, in bytes.
|
||||
int usedBytes;
|
||||
|
||||
/// Returns a shallow copy of this [FileLimitExceededException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
FileLimitExceededException copyWith({
|
||||
bool? instanceFull,
|
||||
int? limitBytes,
|
||||
int? usedBytes,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.FileLimitExceededException',
|
||||
'instanceFull': instanceFull,
|
||||
'limitBytes': limitBytes,
|
||||
'usedBytes': usedBytes,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.FileLimitExceededException',
|
||||
'instanceFull': instanceFull,
|
||||
'limitBytes': limitBytes,
|
||||
'usedBytes': usedBytes,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FileLimitExceededException(instanceFull: $instanceFull, limitBytes: $limitBytes, usedBytes: $usedBytes)';
|
||||
}
|
||||
}
|
||||
|
||||
class _FileLimitExceededExceptionImpl extends FileLimitExceededException {
|
||||
_FileLimitExceededExceptionImpl({
|
||||
required bool instanceFull,
|
||||
required int limitBytes,
|
||||
required int usedBytes,
|
||||
}) : super._(
|
||||
instanceFull: instanceFull,
|
||||
limitBytes: limitBytes,
|
||||
usedBytes: usedBytes,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [FileLimitExceededException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
FileLimitExceededException copyWith({
|
||||
bool? instanceFull,
|
||||
int? limitBytes,
|
||||
int? usedBytes,
|
||||
}) {
|
||||
return FileLimitExceededException(
|
||||
instanceFull: instanceFull ?? this.instanceFull,
|
||||
limitBytes: limitBytes ?? this.limitBytes,
|
||||
usedBytes: usedBytes ?? this.usedBytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// How much room the caller's files take, and how much they may take.
|
||||
abstract class FileUsage
|
||||
implements _is.SerializableModel, _is.ProtocolSerialization {
|
||||
FileUsage._({
|
||||
required this.usedBytes,
|
||||
required this.limitBytes,
|
||||
});
|
||||
|
||||
factory FileUsage({
|
||||
required int usedBytes,
|
||||
required int limitBytes,
|
||||
}) = _FileUsageImpl;
|
||||
|
||||
factory FileUsage.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
return FileUsage(
|
||||
usedBytes: jsonSerialization['usedBytes'] as int,
|
||||
limitBytes: jsonSerialization['limitBytes'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
/// What their files take now, in bytes.
|
||||
int usedBytes;
|
||||
|
||||
/// What they may keep, in bytes: their own limit, or less when the instance
|
||||
/// has less room left.
|
||||
int limitBytes;
|
||||
|
||||
/// Returns a shallow copy of this [FileUsage]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
FileUsage copyWith({
|
||||
int? usedBytes,
|
||||
int? limitBytes,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.FileUsage',
|
||||
'usedBytes': usedBytes,
|
||||
'limitBytes': limitBytes,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.FileUsage',
|
||||
'usedBytes': usedBytes,
|
||||
'limitBytes': limitBytes,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return _is.SerializationManager.encode(this);
|
||||
}
|
||||
}
|
||||
|
||||
class _FileUsageImpl extends FileUsage {
|
||||
_FileUsageImpl({
|
||||
required int usedBytes,
|
||||
required int limitBytes,
|
||||
}) : super._(
|
||||
usedBytes: usedBytes,
|
||||
limitBytes: limitBytes,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [FileUsage]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
FileUsage copyWith({
|
||||
int? usedBytes,
|
||||
int? limitBytes,
|
||||
}) {
|
||||
return FileUsage(
|
||||
usedBytes: usedBytes ?? this.usedBytes,
|
||||
limitBytes: limitBytes ?? this.limitBytes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
|
||||
/// The file asked for can't be had: no row of that ID the caller may read, a
|
||||
/// deleted row, or a file not uploaded yet. Said alike for each, so a caller
|
||||
/// learns nothing of rows they can't read.
|
||||
abstract class StoredFileNotFoundException
|
||||
implements
|
||||
_is.SerializableException,
|
||||
_is.SerializableModel,
|
||||
_is.ProtocolSerialization {
|
||||
StoredFileNotFoundException._({required this.what});
|
||||
|
||||
factory StoredFileNotFoundException({required String what}) =
|
||||
_StoredFileNotFoundExceptionImpl;
|
||||
|
||||
factory StoredFileNotFoundException.fromJson(
|
||||
Map<String, dynamic> jsonSerialization,
|
||||
) {
|
||||
return StoredFileNotFoundException(
|
||||
what: jsonSerialization['what'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// Always 'file'.
|
||||
String what;
|
||||
|
||||
/// Returns a shallow copy of this [StoredFileNotFoundException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
StoredFileNotFoundException copyWith({String? what});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.StoredFileNotFoundException',
|
||||
'what': what,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJsonForProtocol() {
|
||||
return {
|
||||
'__className__': 'halkyon_base.StoredFileNotFoundException',
|
||||
'what': what,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StoredFileNotFoundException(what: $what)';
|
||||
}
|
||||
}
|
||||
|
||||
class _StoredFileNotFoundExceptionImpl extends StoredFileNotFoundException {
|
||||
_StoredFileNotFoundExceptionImpl({required String what})
|
||||
: super._(what: what);
|
||||
|
||||
/// Returns a shallow copy of this [StoredFileNotFoundException]
|
||||
/// with some or all fields replaced by the given arguments.
|
||||
@_is.useResult
|
||||
@override
|
||||
StoredFileNotFoundException copyWith({String? what}) {
|
||||
return StoredFileNotFoundException(what: what ?? this.what);
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,10 @@ import 'auth/email_identity.dart' as _id1dpd4g;
|
|||
import 'auth/email_sign_in_refusal.dart' as _ieyvcag1;
|
||||
import 'auth/email_sign_in_refused_exception.dart' as _iztfgag5;
|
||||
import 'auth/sign_in_ways.dart' as _ie6tbh85;
|
||||
import 'files/file_allowance.dart' as _ieulzhzs;
|
||||
import 'files/file_limit_exceeded_exception.dart' as _iaz1i85g;
|
||||
import 'files/file_usage.dart' as _i45yjkiy;
|
||||
import 'files/stored_file_not_found_exception.dart' as _i8o9pcsj;
|
||||
import 'instance/instance_basics.dart' as _i8jixqx6;
|
||||
import 'keys/device_keys.dart' as _iz50cwqw;
|
||||
import 'keys/keys_challenge.dart' as _itebbsfc;
|
||||
|
|
@ -145,6 +149,10 @@ export 'auth/email_identity.dart';
|
|||
export 'auth/email_sign_in_refusal.dart';
|
||||
export 'auth/email_sign_in_refused_exception.dart';
|
||||
export 'auth/sign_in_ways.dart';
|
||||
export 'files/file_allowance.dart';
|
||||
export 'files/file_limit_exceeded_exception.dart';
|
||||
export 'files/file_usage.dart';
|
||||
export 'files/stored_file_not_found_exception.dart';
|
||||
export 'instance/instance_basics.dart';
|
||||
export 'keys/device_keys.dart';
|
||||
export 'keys/keys_challenge.dart';
|
||||
|
|
@ -838,6 +846,56 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'file_allowance',
|
||||
dartName: 'FileAllowance',
|
||||
schema: 'public',
|
||||
module: 'halkyon_base',
|
||||
columns: [
|
||||
_isp.ColumnDefinition(
|
||||
name: 'id',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int?',
|
||||
columnDefault: 'serial',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'authUserId',
|
||||
columnType: _isp.ColumnType.uuid,
|
||||
isNullable: false,
|
||||
dartType: 'UuidValue',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'maxBytes',
|
||||
columnType: _isp.ColumnType.bigint,
|
||||
isNullable: false,
|
||||
dartType: 'int',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'note',
|
||||
columnType: _isp.ColumnType.text,
|
||||
isNullable: true,
|
||||
dartType: 'String?',
|
||||
),
|
||||
],
|
||||
foreignKeys: [],
|
||||
indexes: [
|
||||
_isp.IndexDefinition(
|
||||
indexName: 'file_allowance_person_idx',
|
||||
tableSpace: null,
|
||||
elements: [
|
||||
_isp.IndexElementDefinition(
|
||||
type: _isp.IndexElementDefinitionType.column,
|
||||
definition: 'authUserId',
|
||||
),
|
||||
],
|
||||
type: 'btree',
|
||||
isUnique: true,
|
||||
isPrimary: false,
|
||||
),
|
||||
],
|
||||
managed: true,
|
||||
),
|
||||
_isp.TableDefinition(
|
||||
name: 'public_profile',
|
||||
dartName: 'PublicProfile',
|
||||
|
|
@ -1592,6 +1650,18 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _ie6tbh85.SignInWays) {
|
||||
return _ie6tbh85.SignInWays.fromJson(data) as T;
|
||||
}
|
||||
if (t == _ieulzhzs.FileAllowance) {
|
||||
return _ieulzhzs.FileAllowance.fromJson(data) as T;
|
||||
}
|
||||
if (t == _iaz1i85g.FileLimitExceededException) {
|
||||
return _iaz1i85g.FileLimitExceededException.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i45yjkiy.FileUsage) {
|
||||
return _i45yjkiy.FileUsage.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i8o9pcsj.StoredFileNotFoundException) {
|
||||
return _i8o9pcsj.StoredFileNotFoundException.fromJson(data) as T;
|
||||
}
|
||||
if (t == _i8jixqx6.InstanceBasics) {
|
||||
return _i8jixqx6.InstanceBasics.fromJson(data) as T;
|
||||
}
|
||||
|
|
@ -1882,6 +1952,25 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (t == _is.getType<_ie6tbh85.SignInWays?>()) {
|
||||
return (data != null ? _ie6tbh85.SignInWays.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_ieulzhzs.FileAllowance?>()) {
|
||||
return (data != null ? _ieulzhzs.FileAllowance.fromJson(data) : null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_iaz1i85g.FileLimitExceededException?>()) {
|
||||
return (data != null
|
||||
? _iaz1i85g.FileLimitExceededException.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i45yjkiy.FileUsage?>()) {
|
||||
return (data != null ? _i45yjkiy.FileUsage.fromJson(data) : null) as T;
|
||||
}
|
||||
if (t == _is.getType<_i8o9pcsj.StoredFileNotFoundException?>()) {
|
||||
return (data != null
|
||||
? _i8o9pcsj.StoredFileNotFoundException.fromJson(data)
|
||||
: null)
|
||||
as T;
|
||||
}
|
||||
if (t == _is.getType<_i8jixqx6.InstanceBasics?>()) {
|
||||
return (data != null ? _i8jixqx6.InstanceBasics.fromJson(data) : null)
|
||||
as T;
|
||||
|
|
@ -2219,6 +2308,10 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
|
||||
_iztfgag5.EmailSignInRefusedException => 'EmailSignInRefusedException',
|
||||
_ie6tbh85.SignInWays => 'SignInWays',
|
||||
_ieulzhzs.FileAllowance => 'FileAllowance',
|
||||
_iaz1i85g.FileLimitExceededException => 'FileLimitExceededException',
|
||||
_i45yjkiy.FileUsage => 'FileUsage',
|
||||
_i8o9pcsj.StoredFileNotFoundException => 'StoredFileNotFoundException',
|
||||
_i8jixqx6.InstanceBasics => 'InstanceBasics',
|
||||
_iz50cwqw.DeviceKeys => 'DeviceKeys',
|
||||
_itebbsfc.KeysChallenge => 'KeysChallenge',
|
||||
|
|
@ -2355,6 +2448,14 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return 'EmailSignInRefusedException';
|
||||
case _ie6tbh85.SignInWays():
|
||||
return 'SignInWays';
|
||||
case _ieulzhzs.FileAllowance():
|
||||
return 'FileAllowance';
|
||||
case _iaz1i85g.FileLimitExceededException():
|
||||
return 'FileLimitExceededException';
|
||||
case _i45yjkiy.FileUsage():
|
||||
return 'FileUsage';
|
||||
case _i8o9pcsj.StoredFileNotFoundException():
|
||||
return 'StoredFileNotFoundException';
|
||||
case _i8jixqx6.InstanceBasics():
|
||||
return 'InstanceBasics';
|
||||
case _iz50cwqw.DeviceKeys():
|
||||
|
|
@ -2571,6 +2672,18 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
if (dataClassName == 'SignInWays') {
|
||||
return deserialize<_ie6tbh85.SignInWays>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'FileAllowance') {
|
||||
return deserialize<_ieulzhzs.FileAllowance>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'FileLimitExceededException') {
|
||||
return deserialize<_iaz1i85g.FileLimitExceededException>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'FileUsage') {
|
||||
return deserialize<_i45yjkiy.FileUsage>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'StoredFileNotFoundException') {
|
||||
return deserialize<_i8o9pcsj.StoredFileNotFoundException>(data['data']);
|
||||
}
|
||||
if (dataClassName == 'InstanceBasics') {
|
||||
return deserialize<_i8jixqx6.InstanceBasics>(data['data']);
|
||||
}
|
||||
|
|
@ -2819,6 +2932,8 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
return _iqxum1j8.EmailCodeRequest.t;
|
||||
case _id1dpd4g.EmailIdentity:
|
||||
return _id1dpd4g.EmailIdentity.t;
|
||||
case _ieulzhzs.FileAllowance:
|
||||
return _ieulzhzs.FileAllowance.t;
|
||||
case _ip37g5t3.AccountPassword:
|
||||
return _ip37g5t3.AccountPassword.t;
|
||||
case _i3fad0p6.AccountReset:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ account:
|
|||
- operating:
|
||||
emailCode:
|
||||
- sendCode:
|
||||
files:
|
||||
- startUpload:
|
||||
- finishUpload:
|
||||
- usage:
|
||||
- download:
|
||||
instance:
|
||||
- basics:
|
||||
keys:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:halkyon_base_server/src/files/file_store.dart';
|
||||
import 'package:halkyon_base_server/src/register/accounts_mode.dart';
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ final class HalkyonBase {
|
|||
this.profilePageNote,
|
||||
this.privacyPagePoints = const [],
|
||||
this.accounts = const HoldsRegister(),
|
||||
this.files,
|
||||
});
|
||||
|
||||
/// Which app this server serves, as its apps know it (`learn` for Halkyon
|
||||
|
|
@ -79,6 +81,11 @@ final class HalkyonBase {
|
|||
/// (`AccountsMode.fromEnvironment`).
|
||||
final AccountsMode accounts;
|
||||
|
||||
/// People's files, where the host keeps them (ADR 0043): which of its
|
||||
/// rows name them and what it takes; none where null, and the `files`
|
||||
/// endpoint refuses.
|
||||
final FileStore? files;
|
||||
|
||||
/// Sets [pod] up with [base]. Call it before the pod starts; calling it
|
||||
/// again replaces the configuration (tests do).
|
||||
static void configure(Serverpod pod, HalkyonBase base) => _ofPod[pod] = base;
|
||||
|
|
|
|||
Loading…
Reference in a new issue