📤 Subir Assets
Sube imágenes con metadatos automáticos y optimización en tiempo real:
curl -X POST https://limbo.lefebvre.es/api/assets \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-F "file=@/path/to/image.jpg" \
-F "tags=producto,principal,hero" \
-F "alt_text=Producto destacado del mes" \
-F "force_webp=true" \
-F "generate_variants=true"
async function uploadAsset(file, metadata = {}) {
const formData = new FormData();
formData.append('file', file);
// Metadata opcional
if (metadata.tags) formData.append('tags', metadata.tags);
if (metadata.alt_text) formData.append('alt_text', metadata.alt_text);
if (metadata.force_webp !== undefined) formData.append('force_webp', metadata.force_webp);
if (metadata.generate_variants !== undefined) formData.append('generate_variants', metadata.generate_variants);
try {
const response = await fetch('/api/assets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${await getAuthToken()}`
},
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result = await response.json();
console.log('Asset uploaded:', result);
return result;
} catch (error) {
console.error('Upload error:', error);
throw error;
}
}
// Uso con drag & drop
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
const result = await uploadAsset(file, {
title: file.name.split('.')[0],
is_public: true,
tags: 'galeria,nuevo'
});
// Actualizar UI
addAssetToGallery(result.data);
} catch (error) {
showErrorNotification(`Error subiendo ${file.name}: ${error.message}`);
}
}
}
});
httpClient = $httpClient;
$this->authService = $authService;
$this->apiUrl = $apiUrl;
}
public function uploadAsset(
string $filePath,
array $metadata = []
): array {
$token = $this->authService->getValidToken();
$formData = [
'file' => DataPart::fromPath($filePath)
];
// Agregar metadata
if (isset($metadata['title'])) {
$formData['title'] = $metadata['title'];
}
if (isset($metadata['description'])) {
$formData['description'] = $metadata['description'];
}
if (isset($metadata['tags'])) {
$formData['tags'] = $metadata['tags'];
}
if (isset($metadata['is_public'])) {
$formData['is_public'] = $metadata['is_public'] ? 'true' : 'false';
}
$formDataPart = new FormDataPart($formData);
$response = $this->httpClient->request('POST', $this->apiUrl . '/assets', [
'headers' => [
'Authorization' => 'Bearer ' . $token,
] + $formDataPart->getPreparedHeaders()->toArray(),
'body' => $formDataPart->bodyToIterable()
]);
if ($response->getStatusCode() !== 201) {
throw new \Exception('Asset upload failed: ' . $response->getContent(false));
}
return $response->toArray();
}
public function uploadFromUrl(string $imageUrl, array $metadata = []): array
{
// Descargar imagen
$tempFile = tempnam(sys_get_temp_dir(), 'limbo_asset_');
file_put_contents($tempFile, file_get_contents($imageUrl));
try {
$result = $this->uploadAsset($tempFile, $metadata);
return $result;
} finally {
unlink($tempFile);
}
}
}
// Uso
$assetService = new LimboAssetService($httpClient, $authService, $apiUrl);
try {
$result = $assetService->uploadAsset('/path/to/image.jpg', [
'title' => 'Producto Hero',
'description' => 'Imagen principal del producto',
'tags' => 'producto,hero,principal',
'is_public' => true
]);
echo "Asset subido: " . $result['data']['id'] . "\n";
echo "URL: " . $result['data']['url'] . "\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
💡 Formatos Soportados
- Imágenes: JPEG, PNG, GIF, WebP, SVG, TIFF
- Tamaño máximo: 25MB por archivo
- Resolución máxima: 8K (7680×4320)
📋 Respuesta de Upload
Después de subir un asset, recibirás información completa:
{
"success": true,
"message": "Asset uploaded successfully",
"data": {
"id": "64f7b1a2c8e1234567890abcd",
"filename": "producto-hero.jpg",
"original_filename": "DSC_0001.jpg",
"title": "Producto Hero",
"description": "Imagen principal del producto",
"mime_type": "image/jpeg",
"size": 2048576,
"dimensions": {
"width": 1920,
"height": 1080
},
"url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/view",
"thumbnail_url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/thumbnail",
"download_url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/download",
"tags": ["producto", "hero", "principal"],
"is_public": true,
"metadata": {
"exif": {
"camera": "Canon EOS R5",
"lens": "RF 24-70mm F2.8 L IS USM",
"focal_length": "50mm",
"aperture": "f/2.8",
"iso": 400,
"datetime": "2024-01-15T14:30:22Z"
},
"colors": {
"dominant": "#2C3E50",
"palette": ["#2C3E50", "#3498DB", "#E74C3C", "#F39C12"]
},
"analysis": {
"faces": 0,
"objects": ["product", "background"],
"quality_score": 0.95
}
},
"portal_id": "your-portal-id",
"created_at": "2024-01-15T14:35:18Z",
"updated_at": "2024-01-15T14:35:18Z"
}
}
Campos de la Respuesta
📁 Información Básica
id- Identificador único del assetfilename- Nombre procesado del archivooriginal_filename- Nombre originalmime_type- Tipo MIME del archivosize- Tamaño en bytes
🖼️ URLs de Acceso
url- Vista optimizada de la imagenthumbnail_url- Miniatura (200x200)download_url- Descarga del archivo original
🎨 Análisis Automático
metadata.exif- Datos EXIF de la cámarametadata.colors- Paleta de colores extraídametadata.analysis- Detección de objetos y caras
📋 Listar Assets
Obtén una lista paginada de todos tus assets con filtros y ordenamiento:
# Listar assets con paginación
curl -X GET "https://limbo.lefebvre.es/api/assets?page=1&limit=20" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json"
# Filtrar por nombre y fecha
curl -X GET "https://limbo.lefebvre.es/api/assets?name=producto&dateFrom=2025-01-01&status=active" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json"
Parámetros de Consulta
pagelimitnameuploadedBydateFromdateTostatusportalIdRespuesta de Lista
{
"success": true,
"data": {
"assets": [
{
"id": "64f7b1a2c8e1234567890abcd",
"filename": "producto-hero.jpg",
"title": "Producto Hero",
"description": "Imagen principal del producto",
"url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/view",
"thumbnail_url": "https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd/thumbnail",
"mime_type": "image/jpeg",
"size": 2048576,
"dimensions": {
"width": 1920,
"height": 1080
},
"tags": ["producto", "hero", "principal"],
"is_public": true,
"created_at": "2024-01-15T14:35:18Z"
}
// ... más assets
],
"pagination": {
"current_page": 1,
"total_pages": 5,
"total_items": 94,
"items_per_page": 20,
"has_next": true,
"has_previous": false
}
}
}
🔍 Detalles de Asset
Obtén información completa de un asset específico:
curl -X GET https://limbo.lefebvre.es/api/assets/01234567-89ab-cdef-0123-456789abcdef \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json"
curl -X GET https://limbo.lefebvre.es/api/assets/01234567-89ab-cdef-0123-456789abcdef \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..." \
-H "Content-Type: application/json"</code></pre>
</div>
<div class="tabs-container">
<div class="tabs-header">
<button class="tab-btn active" data-tab="js-detail">JavaScript</button>
<button class="tab-btn" data-tab="php-detail">PHP</button>
</div>
<div class="tab-content active" id="js-detail">
<div class="code-block">
<div class="code-header">
<span class="code-lang">JavaScript</span>
<button class="copy-btn" data-copy="js-detail-code">Copiar</button>
</div>
<pre class="docs-code-block"><code class="docs-code-inline" id="js-detail-code">async function getAssetDetails(assetId) {
try {
const response = await fetch(`/api/assets/${assetId}`, {
headers: {
'Authorization': `Bearer ${await getAuthToken()}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Failed to get asset: ${response.statusText}`);
}
const result = await response.json();
return result.data;
} catch (error) {
console.error('Error getting asset details:', error);
throw error;
}
}
// Crear preview del asset
function createAssetPreview(asset) {
const preview = document.createElement('div');
preview.className = 'asset-preview';
preview.innerHTML = `
<div class="asset-image">
<img src="${asset.url}" alt="${asset.title}" loading="lazy">
</div>
<div class="asset-info">
<h3>${asset.title}</h3>
<p>${asset.description}</p>
<div class="asset-meta">
<span class="asset-size">${formatFileSize(asset.size)}</span>
<span class="asset-dimensions">${asset.dimensions.width}×${asset.dimensions.height}</span>
<span class="asset-date">${formatDate(asset.created_at)}</span>
</div>
<div class="asset-tags">
${asset.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
</div>
</div>
`;
return preview;
}
public function getAssetDetails(string $assetId): array
{
$token = $this->authService->getValidToken();
$response = $this->httpClient->request('GET', $this->apiUrl . '/assets/' . $assetId, [
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json'
]
]);
if ($response->getStatusCode() !== 200) {
throw new \Exception('Failed to get asset details: ' . $response->getContent(false));
}
return $response->toArray();
}
public function renderAssetGallery(array $assets): string
{
$html = '<div class="assets-gallery">';
foreach ($assets as $asset) {
$html .= sprintf(
'<div class="asset-card" data-id="%s">
<div class="asset-thumbnail">
<img src="%s" alt="%s" loading="lazy">
</div>
<div class="asset-details">
<h4>%s</h4>
<p>%s</p>
<div class="asset-meta">
<span>%s</span>
<span>%dx%d</span>
</div>
<div class="asset-actions">
<a href="%s" target="_blank" class="btn btn-sm">Ver</a>
<a href="%s" download class="btn btn-sm">Descargar</a>
</div>
</div>
</div>',
htmlspecialchars($asset['id']),
htmlspecialchars($asset['thumbnail_url']),
htmlspecialchars($asset['title']),
htmlspecialchars($asset['title']),
htmlspecialchars($asset['description']),
$this->formatFileSize($asset['size']),
$asset['dimensions']['width'],
$asset['dimensions']['height'],
htmlspecialchars($asset['url']),
htmlspecialchars($asset['download_url'])
);
}
$html .= '</div>';
return $html;
}
✏️ Actualizar Assets
Modifica metadata de assets existentes:
curl -X PUT https://limbo.lefebvre.es/assets/64f7b1a2c8e1234567890abcd \
-H "Authorization: Bearer your-jwt-token" \
-H "Content-Type: application/json" \
-d '{
"title": "Nuevo Título",
"description": "Nueva descripción actualizada",
"tags": "actualizado,nuevo,metadata",
"is_public": false
}'
Limitaciones de Actualización
Solo se puede actualizar metadata (título, descripción, tags, visibilidad). Para cambiar el archivo, sube un nuevo asset y elimina el anterior.
🗑️ Eliminar Assets
Elimina permanentemente assets de tu biblioteca:
curl -X DELETE https://limbo.lefebvre.es/api/assets/01234567-89ab-cdef-0123-456789abcdef \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
async function deleteAsset(assetId) {
if (!confirm('¿Estás seguro de eliminar este asset? Esta acción no se puede deshacer.')) {
return false;
}
try {
const response = await fetch(`/api/assets/${assetId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${await getAuthToken()}`
}
});
if (!response.ok) {
throw new Error(`Delete failed: ${response.statusText}`);
}
// Remover del DOM
const assetElement = document.querySelector(`[data-asset-id="${assetId}"]`);
if (assetElement) {
assetElement.remove();
}
showSuccessNotification('Asset eliminado correctamente');
return true;
} catch (error) {
console.error('Error deleting asset:', error);
showErrorNotification('Error al eliminar el asset');
throw error;
}
}
Eliminación Permanente
La eliminación es irreversible. Todos los archivos asociados (original, variantes, thumbnails) se eliminarán permanentemente.
🎯 Casos de Uso Comunes
Galería de Imágenes
Implementa galerías dinámicas con lazy loading, filtros por tags y búsqueda.
Upload Móvil
Sube fotos desde dispositivos móviles con compresión automática.
Automatización
Procesa lotes de imágenes con scripts automáticos y workflows.
CMS Integration
Integra como biblioteca de medios en tu sistema de gestión de contenidos.