Coreccion del cliente para descargar los archivos multimedia
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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
BIN
backend/celerybeat-schedule
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user