initial commit
This commit is contained in:
133
fluxer_api/src/attachment/AttachmentDecayRepository.ts
Normal file
133
fluxer_api/src/attachment/AttachmentDecayRepository.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {AttachmentID, ChannelID, MessageID} from '~/BrandedTypes';
|
||||
import {BatchBuilder, fetchMany, fetchOne} from '~/database/Cassandra';
|
||||
import {AttachmentDecayByExpiry, AttachmentDecayById} from '~/Tables';
|
||||
import type {AttachmentDecayRow} from '~/types/AttachmentDecayTypes';
|
||||
|
||||
interface AttachmentDecayExpiryRow {
|
||||
expiry_bucket: number;
|
||||
expires_at: Date;
|
||||
attachment_id: AttachmentID;
|
||||
channel_id: ChannelID;
|
||||
message_id: MessageID;
|
||||
}
|
||||
|
||||
const FETCH_BY_ID_CQL = AttachmentDecayById.selectCql({
|
||||
where: AttachmentDecayById.where.eq('attachment_id'),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const createFetchExpiredByBucketQuery = (limit: number) =>
|
||||
AttachmentDecayByExpiry.selectCql({
|
||||
where: [
|
||||
AttachmentDecayByExpiry.where.eq('expiry_bucket'),
|
||||
AttachmentDecayByExpiry.where.lte('expires_at', 'current_time'),
|
||||
],
|
||||
limit,
|
||||
});
|
||||
|
||||
export class AttachmentDecayRepository {
|
||||
async upsert(record: AttachmentDecayRow & {expiry_bucket: number}): Promise<void> {
|
||||
const batch = new BatchBuilder();
|
||||
batch.addPrepared(AttachmentDecayById.upsertAll(record));
|
||||
batch.addPrepared(
|
||||
AttachmentDecayByExpiry.upsertAll({
|
||||
expiry_bucket: record.expiry_bucket,
|
||||
expires_at: record.expires_at,
|
||||
attachment_id: record.attachment_id,
|
||||
channel_id: record.channel_id,
|
||||
message_id: record.message_id,
|
||||
}),
|
||||
);
|
||||
await batch.execute();
|
||||
}
|
||||
|
||||
async fetchById(attachmentId: AttachmentID): Promise<AttachmentDecayRow | null> {
|
||||
const row = await fetchOne<AttachmentDecayRow>(FETCH_BY_ID_CQL, {attachment_id: attachmentId});
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
async fetchExpiredByBucket(bucket: number, currentTime: Date, limit = 200): Promise<Array<AttachmentDecayExpiryRow>> {
|
||||
const query = createFetchExpiredByBucketQuery(limit);
|
||||
return fetchMany(query, {expiry_bucket: bucket, current_time: currentTime});
|
||||
}
|
||||
|
||||
async deleteRecords(params: {expiry_bucket: number; expires_at: Date; attachment_id: AttachmentID}): Promise<void> {
|
||||
const batch = new BatchBuilder();
|
||||
batch.addPrepared(
|
||||
AttachmentDecayByExpiry.deleteByPk({
|
||||
expiry_bucket: params.expiry_bucket,
|
||||
expires_at: params.expires_at,
|
||||
attachment_id: params.attachment_id,
|
||||
}),
|
||||
);
|
||||
batch.addPrepared(AttachmentDecayById.deleteByPk({attachment_id: params.attachment_id}));
|
||||
await batch.execute();
|
||||
}
|
||||
|
||||
async fetchAllByBucket(bucket: number, limit = 200): Promise<Array<AttachmentDecayExpiryRow>> {
|
||||
const query = AttachmentDecayByExpiry.selectCql({
|
||||
where: [AttachmentDecayByExpiry.where.eq('expiry_bucket')],
|
||||
limit,
|
||||
});
|
||||
return fetchMany<AttachmentDecayExpiryRow>(query, {expiry_bucket: bucket});
|
||||
}
|
||||
|
||||
async deleteAllByBucket(bucket: number): Promise<number> {
|
||||
const records = await this.fetchAllByBucket(bucket);
|
||||
if (records.length === 0) return 0;
|
||||
|
||||
const batch = new BatchBuilder();
|
||||
for (const record of records) {
|
||||
batch.addPrepared(
|
||||
AttachmentDecayByExpiry.deleteByPk({
|
||||
expiry_bucket: record.expiry_bucket,
|
||||
expires_at: record.expires_at,
|
||||
attachment_id: record.attachment_id,
|
||||
}),
|
||||
);
|
||||
batch.addPrepared(AttachmentDecayById.deleteByPk({attachment_id: record.attachment_id}));
|
||||
}
|
||||
await batch.execute();
|
||||
return records.length;
|
||||
}
|
||||
|
||||
async clearAll(days = 30): Promise<number> {
|
||||
let totalDeleted = 0;
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const date = new Date();
|
||||
date.setUTCDate(date.getUTCDate() - i);
|
||||
const bucket = parseInt(
|
||||
`${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, '0')}${String(date.getUTCDate()).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}`,
|
||||
10,
|
||||
);
|
||||
|
||||
const deletedInBucket = await this.deleteAllByBucket(bucket);
|
||||
totalDeleted += deletedInBucket;
|
||||
}
|
||||
|
||||
return totalDeleted;
|
||||
}
|
||||
}
|
||||
142
fluxer_api/src/attachment/AttachmentDecayService.ts
Normal file
142
fluxer_api/src/attachment/AttachmentDecayService.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {AttachmentID, ChannelID, MessageID} from '~/BrandedTypes';
|
||||
import {Config} from '~/Config';
|
||||
import type {AttachmentDecayRow} from '~/types/AttachmentDecayTypes';
|
||||
import {
|
||||
computeCost,
|
||||
computeDecay,
|
||||
DEFAULT_DECAY_CONSTANTS,
|
||||
DEFAULT_RENEWAL_CONSTANTS,
|
||||
extendExpiry,
|
||||
getExpiryBucket,
|
||||
maybeRenewExpiry,
|
||||
} from '~/utils/AttachmentDecay';
|
||||
import {AttachmentDecayRepository} from './AttachmentDecayRepository';
|
||||
|
||||
export interface AttachmentDecayPayload {
|
||||
attachmentId: AttachmentID;
|
||||
channelId: ChannelID;
|
||||
messageId: MessageID;
|
||||
filename: string;
|
||||
sizeBytes: bigint;
|
||||
uploadedAt: Date;
|
||||
currentExpiresAt?: Date | null;
|
||||
}
|
||||
|
||||
export class AttachmentDecayService {
|
||||
constructor(private readonly repo: AttachmentDecayRepository = new AttachmentDecayRepository()) {}
|
||||
|
||||
async upsertMany(payloads: Array<AttachmentDecayPayload>): Promise<void> {
|
||||
if (!Config.attachmentDecayEnabled) return;
|
||||
|
||||
for (const payload of payloads) {
|
||||
const decay = computeDecay({sizeBytes: payload.sizeBytes, uploadedAt: payload.uploadedAt});
|
||||
if (!decay) continue;
|
||||
|
||||
const expiresAt = extendExpiry(payload.currentExpiresAt ?? null, decay.expiresAt);
|
||||
const record: AttachmentDecayRow & {expiry_bucket: number} = {
|
||||
attachment_id: payload.attachmentId,
|
||||
channel_id: payload.channelId,
|
||||
message_id: payload.messageId,
|
||||
filename: payload.filename,
|
||||
size_bytes: payload.sizeBytes,
|
||||
uploaded_at: payload.uploadedAt,
|
||||
expires_at: expiresAt,
|
||||
last_accessed_at: payload.uploadedAt,
|
||||
cost: decay.cost,
|
||||
lifetime_days: decay.days,
|
||||
status: null,
|
||||
expiry_bucket: getExpiryBucket(expiresAt),
|
||||
};
|
||||
await this.repo.upsert(record);
|
||||
}
|
||||
}
|
||||
|
||||
async extendForAttachments(attachments: Array<AttachmentDecayPayload>): Promise<void> {
|
||||
if (!Config.attachmentDecayEnabled) return;
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const existing = await this.repo.fetchById(attachment.attachmentId);
|
||||
if (!existing) continue;
|
||||
const now = new Date();
|
||||
if (existing.expires_at.getTime() <= now.getTime()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uploadedAt = existing.uploaded_at;
|
||||
const decay = computeDecay({sizeBytes: attachment.sizeBytes, uploadedAt});
|
||||
if (!decay) continue;
|
||||
|
||||
let expiresAt = extendExpiry(existing.expires_at ?? null, decay.expiresAt);
|
||||
|
||||
const windowDays = DEFAULT_RENEWAL_CONSTANTS.RENEW_WINDOW_DAYS;
|
||||
const thresholdDays = DEFAULT_RENEWAL_CONSTANTS.RENEW_THRESHOLD_DAYS;
|
||||
|
||||
const renewed = maybeRenewExpiry({
|
||||
currentExpiry: expiresAt,
|
||||
now,
|
||||
thresholdDays,
|
||||
windowDays,
|
||||
});
|
||||
|
||||
if (renewed) {
|
||||
expiresAt = renewed;
|
||||
}
|
||||
|
||||
const lifetimeDays = Math.round((expiresAt.getTime() - uploadedAt.getTime()) / (1000 * 60 * 60 * 24));
|
||||
const cost = computeCost({
|
||||
sizeBytes: attachment.sizeBytes,
|
||||
lifetimeDays,
|
||||
pricePerTBPerMonth: DEFAULT_DECAY_CONSTANTS.PRICE_PER_TB_PER_MONTH,
|
||||
});
|
||||
|
||||
await this.repo.upsert({
|
||||
attachment_id: attachment.attachmentId,
|
||||
channel_id: attachment.channelId,
|
||||
message_id: attachment.messageId,
|
||||
filename: attachment.filename,
|
||||
size_bytes: attachment.sizeBytes,
|
||||
uploaded_at: uploadedAt,
|
||||
expires_at: expiresAt,
|
||||
last_accessed_at: now,
|
||||
cost,
|
||||
lifetime_days: lifetimeDays,
|
||||
status: existing.status ?? null,
|
||||
expiry_bucket: getExpiryBucket(expiresAt),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMetadata(
|
||||
attachments: Array<Pick<AttachmentDecayPayload, 'attachmentId'>>,
|
||||
): Promise<Map<string, AttachmentDecayRow>> {
|
||||
if (!Config.attachmentDecayEnabled) return new Map();
|
||||
|
||||
const map = new Map<string, AttachmentDecayRow>();
|
||||
for (const att of attachments) {
|
||||
const row = await this.repo.fetchById(att.attachmentId);
|
||||
if (row) {
|
||||
map.set(att.attachmentId.toString(), row);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user