Coreccion del cliente para descargar los archivos multimedia

This commit is contained in:
2026-02-24 14:00:28 -06:00
parent c9f934a0da
commit ad26062f46
6 changed files with 114 additions and 24 deletions

View File

@@ -244,8 +244,10 @@ def sync_from_hub_task():
# Download assets after bulk update (Polling)
from .utils import download_file_from_hub, sync_assets_from_content
for art_data in articles_data:
if art_data.get('file_url'):
# art_data contains the virtual fields because it was dumped via HelpArticleInDB
if "file_url" in art_data and art_data['file_url']:
download_file_from_hub(art_data['file_url'])
sync_assets_from_content(art_data.get('content', ''))
logger.info("Polling sync completed successfully.")

View File

@@ -29,6 +29,7 @@ def download_file_from_hub(relative_path: str) -> bool:
local_path = Path(clean_path)
if local_path.exists():
logger.info(f"File {clean_path} already exists, skipping download.")
return True
# Ensure directories exist
@@ -38,7 +39,9 @@ def download_file_from_hub(relative_path: str) -> bool:
# CENTRAL_SERVER_URL is usually http://hub:8000/api/v1/core/help-center/sync/
# We want http://hub:8000/api/
base_url = settings.CENTRAL_SERVER_URL.split("/v1/")[0]
hub_file_url = f"{base_url}/uploads/{clean_path.replace('uploads/', '')}"
# The file in backend is served usually under /api/uploads/...
# But clean_path is just "uploads/...". So the Hub route is base_url + "/" + clean_path
hub_file_url = f"{base_url}/{clean_path}"
logger.info(f"Downloading asset from Hub: {hub_file_url} -> {local_path}")
@@ -51,7 +54,7 @@ def download_file_from_hub(relative_path: str) -> bool:
logger.info(f"Successfully downloaded {clean_path}")
return True
else:
logger.warning(f"Failed to download {clean_path}: Status {response.status_code}")
logger.warning(f"Failed to download {clean_path}: Status {response.status_code} URL: {hub_file_url}")
return False
except Exception as e:
logger.error(f"Error downloading {clean_path}: {str(e)}")

BIN
backend/celerybeat-schedule Normal file

Binary file not shown.

View File

@@ -275,6 +275,7 @@ services:
memory: 1G
reservations:
memory: 512M
# celery
celery_worker:
build: ./backend
@@ -294,6 +295,10 @@ services:
depends_on:
- backend
- valkey
volumes:
- ./backend:/app
- backend_cache:/app/__pycache__
- backend_uploads:/app/uploads
networks:
- backend-net
@@ -315,6 +320,10 @@ services:
depends_on:
- backend
- valkey
volumes:
- ./backend:/app
- backend_cache:/app/__pycache__
- backend_uploads:/app/uploads
networks:
- backend-net

View File

@@ -16,6 +16,25 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Helper to resolve an API path from the DB (e.g., /api/uploads/...) to a full URL pointing to the local backend.
function resolveAssetUrl(url: string | undefined): string {
try {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
// Fallback to empty string if VITE_API_URL is missing
// @ts-ignore
const apiUrl = import.meta.env.VITE_API_URL || '';
const base = apiUrl.replace(/\/$/, '');
const cleanUrl = url.replace(/^\/api\//, '/');
const finalUrl = `${base}${cleanUrl}`;
return finalUrl;
} catch (e) {
console.error('[SSR DEBUG] Error in resolveAssetUrl:', e);
return '';
}
}
let article: HelpArticle | null = null; // Removed $state() as it's not reactive by itself in Svelte 5 standard store usage unless wrapped
let loading = true; // Use standard value, reactive via variable assignment in Svelte 5 runes if enabled
let error: string | null = null;
@@ -60,16 +79,32 @@
import { browser } from '$app/environment';
function renderMarkdown(content: string) {
const rawHtml = marked.parse(content || '') as string;
// Add IDs to headers for TOC navigation
const htmlWithIds = rawHtml.replace(/<h([1-3])>(.*?)<\/h\1>/g, (match, level, text) => {
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>`;
});
if (browser) {
return DOMPurify.sanitize(htmlWithIds);
if (!content) return '';
try {
const rawHtml = marked.parse(content) as string;
// Add IDs to headers for TOC navigation
let htmlWithIds = rawHtml.replace(/<h([1-3])>(.*?)<\/h\1>/g, (match, level, text) => {
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
return `<h${level} id="${id}">${text}</h${level}>`;
});
// Rewrite image src and link hrefs to point to the local backend
htmlWithIds = htmlWithIds.replace(/src="([^"]+)"/g, (match, src) => {
return `src="${resolveAssetUrl(src)}"`;
});
htmlWithIds = htmlWithIds.replace(/href="(\/api\/uploads\/[^"]+)"/g, (match, href) => {
return `href="${resolveAssetUrl(href)}"`;
});
if (browser && typeof window !== 'undefined') {
// DOMPurify only works in the browser
return DOMPurify.sanitize(htmlWithIds);
}
return htmlWithIds; // SSR returns raw HTML (or could use isomorphic-dompurify)
} catch (e) {
console.error('Markdown parsing error:', e);
return '';
}
return htmlWithIds;
}
function scrollToHeader(id: string) {
@@ -152,11 +187,20 @@
</div>
</div>
<div class="flex gap-2">
<Button variant="outline" size="sm" href={article.file_url} target="_blank">
<Button
variant="outline"
size="sm"
href={resolveAssetUrl(article.file_url)}
target="_blank"
>
<ExternalLink class="mr-2 h-4 w-4" />
Abrir en pestaña
</Button>
<Button size="sm" href={article.file_url} download={article.title}>
<Button
size="sm"
href={resolveAssetUrl(article.file_url)}
download={article.title}
>
<Download class="mr-2 h-4 w-4" />
Descargar
</Button>
@@ -164,7 +208,7 @@
</div>
<!-- PDF Embed -->
<iframe
src={article.file_url}
src={resolveAssetUrl(article.file_url)}
title={article.title}
class="h-[800px] w-full rounded-xl border shadow-inner"
></iframe>
@@ -172,7 +216,7 @@
{:else if article.content_type === 'video'}
<div class="flex flex-col gap-4">
<video controls class="w-full rounded-xl border shadow-lg">
<source src={article.file_url} type={article.mime_type} />
<source src={resolveAssetUrl(article.file_url)} type={article.mime_type} />
Tu navegador no soporta videos.
</video>
<div class="rounded-lg border bg-muted/30 p-4">
@@ -193,7 +237,11 @@
Este archivo no tiene vista previa directa.
</p>
</div>
<Button href={article.file_url} download={article.title} class="px-8 shadow-lg">
<Button
href={resolveAssetUrl(article.file_url)}
download={article.title}
class="px-8 shadow-lg"
>
<Download class="mr-2 h-4 w-4" />
Descargar ahora
</Button>

View File

@@ -27,6 +27,19 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
// Helper to resolve an API path from the DB (e.g., /api/uploads/...) to a full URL pointing to the local backend.
function resolveAssetUrl(url: string | undefined): string {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
// Fallback to empty string if VITE_API_URL is missing
// @ts-ignore
const apiUrl = import.meta.env.VITE_API_URL || '';
const base = apiUrl.replace(/\/$/, '');
const cleanUrl = url.replace(/^\/api\//, '/');
return `${base}${cleanUrl}`;
}
let article = $state<HelpArticle | null>(null);
let loading = $state(true);
let processing = $state(false);
@@ -206,11 +219,26 @@
}
function renderMarkdown(text: string) {
const html = marked.parse(text || '') as string;
if (browser) {
return DOMPurify.sanitize(html);
if (!text) return '';
try {
const html = marked.parse(text) as string;
// Rewrite image src and link hrefs to point to the local backend
let resolvedHtml = html.replace(/src="([^"]+)"/g, (match, src) => {
return `src="${resolveAssetUrl(src)}"`;
});
resolvedHtml = resolvedHtml.replace(/href="(\/api\/uploads\/[^"]+)"/g, (match, href) => {
return `href="${resolveAssetUrl(href)}"`;
});
if (browser && typeof window !== 'undefined') {
return DOMPurify.sanitize(resolvedHtml);
}
return resolvedHtml;
} catch (e) {
console.error("Markdown parsing error in editor:", e);
return '';
}
return html;
}
</script>
@@ -359,8 +387,8 @@
bind:value={content}
class="w-full flex-1 resize-none bg-background p-8 font-mono text-sm leading-relaxed outline-none"
placeholder="# Empieza a escribir aquí..."
ondrop={handleDrop}
ondragover={(e) => e.preventDefault()}
ondrop={(e: DragEvent) => handleDrop(e)}
ondragover={(e: DragEvent) => e.preventDefault()}
></textarea>
<!-- Dropping Hint -->
@@ -418,7 +446,7 @@
</div>
</div>
<div class="flex gap-2">
<Button variant="outline" class="flex-1" href={file_url} target="_blank">
<Button variant="outline" class="flex-1" href={resolveAssetUrl(file_url)} target="_blank">
Ver Archivo
</Button>
<label