fix: download bunny materials through academy
This commit is contained in:
@@ -73,6 +73,29 @@ export async function uploadBunnyAsset(asset: StoredAsset) {
|
||||
return { key, assetUrl: `${cdnHost}/${key}` };
|
||||
}
|
||||
|
||||
export function bunnyAssetKeyFromUrl(url: string) {
|
||||
if (!config.BUNNY_STORAGE_CDN_HOST) return null;
|
||||
const cdnHost = config.BUNNY_STORAGE_CDN_HOST.replace(/\/$/, '');
|
||||
if (!url.startsWith(`${cdnHost}/materials/`)) return null;
|
||||
const key = url.slice(cdnHost.length + 1);
|
||||
return /^materials\/[0-9a-f-]{36}\.(?:pdf|docx|doc|xlsx|xls|csv|zip)$/i.test(key) ? key : null;
|
||||
}
|
||||
|
||||
export async function downloadBunnyAsset(key: string) {
|
||||
const { zone, password, endpoint } = bunnyStorageConfiguration();
|
||||
const response = await fetch(`${endpoint}/${encodeURIComponent(zone)}/${key}`, {
|
||||
headers: { AccessKey: password },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text().catch(() => '');
|
||||
throw new BunnyStorageRequestError(`Bunny Storage download failed (${response.status})${detail ? `: ${detail.slice(0, 250)}` : ''}`);
|
||||
}
|
||||
return {
|
||||
body: Buffer.from(await response.arrayBuffer()),
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
|
||||
export async function downloadBunnyCover(filename: string) {
|
||||
const { zone, password, endpoint } = bunnyStorageConfiguration();
|
||||
const response = await fetch(`${endpoint}/${encodeURIComponent(zone)}/covers/${encodeURIComponent(filename)}`, {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FastifyPluginAsync } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { pool } from '../db/pool.js';
|
||||
import { signedBunnyEmbedUrl, BunnyConfigurationError } from '../providers/bunny.js';
|
||||
import { BunnyStorageConfigurationError, BunnyStorageRequestError, downloadBunnyCover } from '../providers/bunny-storage.js';
|
||||
import { BunnyStorageConfigurationError, BunnyStorageRequestError, bunnyAssetKeyFromUrl, downloadBunnyAsset, downloadBunnyCover } from '../providers/bunny-storage.js';
|
||||
|
||||
const paramsSchema = z.object({
|
||||
courseId: z.string().uuid(),
|
||||
@@ -14,6 +14,7 @@ const playbackParamsSchema = z.object({
|
||||
const coverParamsSchema = z.object({
|
||||
filename: z.string().regex(/^[0-9a-f]{8}-[0-9a-f-]{27}\.(?:jpg|png|webp)$/i),
|
||||
});
|
||||
const assetParamsSchema = z.object({ assetId: z.string().uuid() });
|
||||
|
||||
export const courseSelect = (publicOnly = false, includeUnreadyMedia = false) => `
|
||||
select
|
||||
@@ -133,6 +134,36 @@ export const courseRoutes: FastifyPluginAsync = async (app) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/assets/:assetId/download', { preHandler: app.authenticate }, async (request, reply) => {
|
||||
const { assetId } = assetParamsSchema.parse(request.params);
|
||||
const result = await pool.query<{ name: string; url: string; status: string; instructor_id: string }>(
|
||||
`select a.name, a.url, c.status, c.instructor_id
|
||||
from assets a
|
||||
join courses c on c.id = coalesce(a.course_id, (select lesson.course_id from lessons lesson where lesson.id = a.lesson_id))
|
||||
where a.id = $1`,
|
||||
[assetId],
|
||||
);
|
||||
const asset = result.rows[0];
|
||||
if (!asset || (asset.status !== 'published' && request.user.role !== 'admin' && asset.instructor_id !== request.user.id)) {
|
||||
return reply.code(404).send({ error: 'Material não encontrado.' });
|
||||
}
|
||||
const key = bunnyAssetKeyFromUrl(asset.url);
|
||||
if (!key) return reply.redirect(asset.url);
|
||||
try {
|
||||
const file = await downloadBunnyAsset(key);
|
||||
const filename = asset.name.replace(/[\\/\r\n"]/g, '_');
|
||||
reply.header('Content-Type', file.contentType);
|
||||
reply.header('Content-Disposition', `attachment; filename="${filename}"; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||||
reply.header('Cache-Control', 'private, no-store');
|
||||
return reply.send(file.body);
|
||||
} catch (error) {
|
||||
if (error instanceof BunnyStorageConfigurationError || error instanceof BunnyStorageRequestError) {
|
||||
return reply.code(404).send({ error: 'Material não encontrado.' });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/', async (request) => {
|
||||
const authenticated = await hasSession(request);
|
||||
const result = await pool.query(
|
||||
|
||||
Reference in New Issue
Block a user