Skip to main content
Glama
images.js•21 kB
const fetch = require('node-fetch'); const fs = require('fs'); const FormData = require('form-data'); const { logger } = require('../utils/logger'); class ImageAPI { constructor(client) { this.client = client; } async uploadImages(vehicleId, imageInputs, mainImageIndex = 0) { const uploadedImages = []; const errors = []; logger.info(`Starting batch image upload for vehicle ${vehicleId}`, { totalImages: imageInputs.length, mainImageIndex: mainImageIndex }); // Check if vehicle has existing images to determine if we should auto-set main image let vehicleHasImages = false; try { const existingImages = await this.getVehicleImages(vehicleId); vehicleHasImages = existingImages.hasImages; logger.info(`Vehicle ${vehicleId} currently has ${existingImages.imageCount} images`, { hasImages: vehicleHasImages }); } catch (error) { logger.warn(`Could not check existing images for vehicle ${vehicleId}:`, error.message); // Continue with upload anyway } // Upload each image - handle both file paths and URLs for (let i = 0; i < imageInputs.length; i++) { const imageInput = imageInputs[i]; try { let result; // Handle different input types if (typeof imageInput === 'string') { // File path, URL, or data URI if (imageInput.startsWith('http://') || imageInput.startsWith('https://')) { // URL result = await this.uploadImageFromUrl(vehicleId, imageInput, i === mainImageIndex); } else if (imageInput.startsWith('data:image/')) { // Data URI (MCP resource reference) result = await this.uploadImageFromDataUri(vehicleId, imageInput, i === mainImageIndex); } else { // File path if (!fs.existsSync(imageInput)) { throw new Error(`File not found: ${imageInput}`); } // Create form data const form = new FormData(); form.append('file', fs.createReadStream(imageInput)); // Upload the image const token = await this.client.auth.getToken(); const country = process.env.STOCKSPARK_COUNTRY || 'it'; const apiUrl = process.env.STOCKSPARK_API_URL || 'https://carspark-api.dealerk.com'; const url = `${apiUrl}/${country}/vehicle/${vehicleId}/images/gallery/upload`; logger.info(`Uploading image from file: ${imageInput} to vehicle ${vehicleId}`); logger.apiRequest('POST', url, { type: 'file_upload', filename: imageInput }); const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, ...form.getHeaders() }, body: form }); logger.apiResponse(response.status, response.statusText, url); if (!response.ok) { const errorText = await response.text(); logger.error(`Image upload failed for vehicle ${vehicleId}`, { status: response.status, statusText: response.statusText, error: errorText, filename: imageInput }); throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`); } const uploadResult = await response.json(); logger.info(`Successfully uploaded image to vehicle ${vehicleId}`, { imageId: uploadResult.id || uploadResult.imageId, filename: imageInput }); result = { success: true, imageId: uploadResult.id || uploadResult.imageId, path: imageInput }; // Only set as main if explicitly requested via mainImageIndex if (i === mainImageIndex && uploadResult.id) { await this.setMainImage(vehicleId, uploadResult.id); logger.info(`Set image ${uploadResult.id} as main for vehicle ${vehicleId}`, { reason: 'specified_main_index' }); } } } if (result.success) { const isMainImage = (i === mainImageIndex); uploadedImages.push({ ...result, index: i, main: isMainImage }); } else { errors.push({ path: imageInput.path || imageInput.uri || `input_${i}`, error: result.error }); } } catch (error) { errors.push({ path: typeof imageInput === 'string' ? imageInput : imageInput.uri || `input_${i}`, error: error.message }); } } const result = { vehicleId, uploadedCount: uploadedImages.length, uploadedImages, errors, success: errors.length === 0 }; logger.info(`Completed batch image upload for vehicle ${vehicleId}`, { totalRequested: imageInputs.length, successfulUploads: uploadedImages.length, failedUploads: errors.length, success: result.success }); return result; } async checkMainImageStatus(vehicleId) { try { const imageInfo = await this.getVehicleImages(vehicleId); const hasMainImage = imageInfo.images.some(img => img.main === true); const firstImageId = imageInfo.images.length > 0 ? imageInfo.images[0].id : null; return { hasImages: imageInfo.hasImages, hasMainImage: hasMainImage, firstImageId: firstImageId, totalImages: imageInfo.imageCount }; } catch (error) { logger.warn(`Failed to check main image status for vehicle ${vehicleId}:`, error.message); return { hasImages: false, hasMainImage: false, firstImageId: null, totalImages: 0 }; } } async uploadImageFromDataUri(vehicleId, dataUri, setAsMain = false) { try { // Parse data URI const matches = dataUri.match(/^data:image\/([^;]+);base64,(.+)$/); if (!matches) { throw new Error('Invalid data URI format'); } const mimeType = `image/${matches[1]}`; const base64Data = matches[2]; const buffer = Buffer.from(base64Data, 'base64'); // Create form data with buffer const form = new FormData(); const filename = `upload_${Date.now()}.${matches[1]}`; form.append('file', buffer, { filename, contentType: mimeType }); // Upload the image const token = await this.client.auth.getToken(); const country = process.env.STOCKSPARK_COUNTRY || 'it'; const apiUrl = process.env.STOCKSPARK_API_URL || 'https://carspark-api.dealerk.com'; const url = `${apiUrl}/${country}/vehicle/${vehicleId}/images/gallery/upload`; logger.info(`Uploading image from data URI to vehicle ${vehicleId}`, { size: buffer.length, type: mimeType }); logger.apiRequest('POST', url, { type: 'data_uri_upload', size: buffer.length }); const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, ...form.getHeaders() }, body: form }); logger.apiResponse(response.status, response.statusText, url); if (!response.ok) { const errorText = await response.text(); logger.error(`Data URI upload failed for vehicle ${vehicleId}`, { status: response.status, statusText: response.statusText, error: errorText, size: buffer.length }); throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`); } const result = await response.json(); logger.info(`Successfully uploaded data URI to vehicle ${vehicleId}`, { imageId: result.id || result.imageId, size: buffer.length }); // If requested, set as main image if (setAsMain && result.id) { await this.setMainImage(vehicleId, result.id); } return { success: true, imageId: result.id || result.imageId, dataUri: `${dataUri.substring(0, 50)}...` // Truncated for logging }; } catch (error) { return { success: false, error: error.message, dataUri: `${dataUri.substring(0, 50)}...` // Truncated for logging }; } } async uploadImageFromBase64(vehicleId, base64Data, mimeType, filename = 'image.jpg', setAsMain = false) { try { // Validate inputs if (!base64Data || typeof base64Data !== 'string') { throw new Error('Invalid base64 data: must be a non-empty string'); } if (!mimeType || !mimeType.startsWith('image/')) { throw new Error('Invalid MIME type: must be an image type (e.g., image/png, image/jpeg)'); } // Remove data URL prefix if present const cleanBase64 = base64Data.replace(/^data:image\/(png|jpeg|jpg|gif|webp);base64,/, ''); // Validate base64 format if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) { throw new Error('Invalid base64 format: contains invalid characters'); } // Check minimum length (a 1x1 pixel image should be at least 50 chars) if (cleanBase64.length < 20) { throw new Error('Invalid base64 data: too short to be a valid image'); } // Convert base64 to buffer const buffer = Buffer.from(cleanBase64, 'base64'); // Validate buffer size if (buffer.length === 0) { throw new Error('Invalid base64 data: decoded to empty buffer'); } // Check file size limits (2MB max for better performance) if (buffer.length > 2 * 1024 * 1024) { throw new Error('Image too large: maximum size is 2MB for optimal performance. Consider resizing the image.'); } // Log image size for debugging const sizeMB = (buffer.length / (1024 * 1024)).toFixed(2); console.log(`📸 Processing image: ${sizeMB}MB (${filename})`); // Create form data with buffer const form = new FormData(); form.append('file', buffer, { filename: filename || 'image.jpg', contentType: mimeType || 'image/jpeg' }); // Upload the image const token = await this.client.auth.getToken(); const country = process.env.STOCKSPARK_COUNTRY || 'it'; const apiUrl = process.env.STOCKSPARK_API_URL || 'https://carspark-api.dealerk.com'; const url = `${apiUrl}/${country}/vehicle/${vehicleId}/images/gallery/upload`; const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, ...form.getHeaders() }, body: form }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`); } const result = await response.json(); // If requested, set as main image if (setAsMain && result.id) { await this.setMainImage(vehicleId, result.id); } return { success: true, imageId: result.id || result.imageId, filename: filename }; } catch (error) { return { success: false, error: error.message, filename: filename }; } } async uploadImagesFromData(vehicleId, imageDataArray, mainImageIndex = 0) { // Validate inputs if (!Array.isArray(imageDataArray)) { throw new Error('imageDataArray must be an array'); } if (imageDataArray.length === 0) { throw new Error('imageDataArray cannot be empty'); } if (imageDataArray.length > 50) { throw new Error('Too many images: maximum 50 images per upload'); } if (mainImageIndex < 0 || mainImageIndex >= imageDataArray.length) { throw new Error(`Invalid mainImageIndex: must be between 0 and ${imageDataArray.length - 1}`); } const uploadedImages = []; const errors = []; // Check if vehicle has existing images to determine if we should auto-set main image let vehicleHasImages = false; try { const existingImages = await this.getVehicleImages(vehicleId); vehicleHasImages = existingImages.hasImages; logger.info(`Vehicle ${vehicleId} currently has ${existingImages.imageCount} images`, { hasImages: vehicleHasImages }); } catch (error) { logger.warn(`Could not check existing images for vehicle ${vehicleId}:`, error.message); // Continue with upload anyway } for (let i = 0; i < imageDataArray.length; i++) { const imageData = imageDataArray[i]; const isMain = i === mainImageIndex; try { // Validate individual image data object if (!imageData || typeof imageData !== 'object') { throw new Error(`Image ${i + 1}: imageData must be an object`); } if (!imageData.data) { throw new Error(`Image ${i + 1}: missing 'data' field`); } if (!imageData.mimeType) { throw new Error(`Image ${i + 1}: missing 'mimeType' field`); } // Only set as main if explicitly requested via mainImageIndex const isMainImage = (i === mainImageIndex); const result = await this.uploadImageFromBase64( vehicleId, imageData.data, imageData.mimeType, imageData.filename || `image_${i + 1}.${imageData.mimeType.split('/')[1]}`, isMainImage ); if (result.success) { uploadedImages.push({ ...result, index: i, main: isMainImage }); } else { errors.push({ index: i, error: result.error }); } } catch (error) { errors.push({ index: i, error: error.message }); } } return { vehicleId, uploadedCount: uploadedImages.length, uploadedImages, errors, success: errors.length === 0 }; } async uploadImageFromUrl(vehicleId, imageUrl, setAsMain = false) { try { // Download image first const imageResponse = await fetch(imageUrl); if (!imageResponse.ok) { throw new Error(`Failed to download image: ${imageResponse.status}`); } const buffer = await imageResponse.buffer(); // Create form data with buffer const form = new FormData(); const filename = imageUrl.split('/').pop() || 'image.jpg'; form.append('file', buffer, { filename }); // Upload the image const token = await this.client.auth.getToken(); const country = process.env.STOCKSPARK_COUNTRY || 'it'; const apiUrl = process.env.STOCKSPARK_API_URL || 'https://carspark-api.dealerk.com'; const url = `${apiUrl}/${country}/vehicle/${vehicleId}/images/gallery/upload`; logger.info(`Uploading image from URL: ${imageUrl} to vehicle ${vehicleId}`); logger.apiRequest('POST', url, { type: 'url_upload', sourceUrl: imageUrl }); const response = await fetch(url, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, ...form.getHeaders() }, body: form }); logger.apiResponse(response.status, response.statusText, url); if (!response.ok) { const errorText = await response.text(); logger.error(`Image upload from URL failed for vehicle ${vehicleId}`, { status: response.status, statusText: response.statusText, error: errorText, sourceUrl: imageUrl }); throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText}`); } const result = await response.json(); logger.info(`Successfully uploaded image from URL to vehicle ${vehicleId}`, { imageId: result.id || result.imageId, sourceUrl: imageUrl }); // If requested, set as main image if (setAsMain && result.id) { await this.setMainImage(vehicleId, result.id); } return { success: true, imageId: result.id || result.imageId, url: imageUrl }; } catch (error) { return { success: false, error: error.message, url: imageUrl }; } } async getVehicleImages(vehicleId) { const vehicle = await this.client.get(`/vehicle/${vehicleId}?withGallery=true`); const images = vehicle.gallery || vehicle.images?.GALLERY_ITEM || []; return { vehicleId, imageCount: images.length, images: images.map((img, index) => ({ id: img.id || index, url: img.url || img, index: img.index || index + 1, main: img.main || false, type: img.type || "GALLERY_ITEM" })), hasImages: images.length > 0 }; } async deleteImage(vehicleId, imageId) { const vehicle = await this.client.get(`/vehicle/${vehicleId}?withGallery=true`); const images = vehicle.gallery || vehicle.images?.GALLERY_ITEM || []; if (images.length === 0) { throw new Error('No images found for this vehicle'); } const initialCount = images.length; // Filter out the image const filteredImages = images.filter( img => (img.id || `image_${images.indexOf(img)}`) !== imageId ); if (filteredImages.length === initialCount) { throw new Error(`Image with ID ${imageId} not found`); } // Update the correct field based on API response structure if (vehicle.gallery) { vehicle.gallery = filteredImages; } else if (vehicle.images?.GALLERY_ITEM) { vehicle.images.GALLERY_ITEM = filteredImages; } // Update vehicle await this.client.put(`/vehicle/${vehicleId}`, vehicle); return { vehicleId, deletedImageId: imageId, remainingImages: filteredImages.length }; } async setMainImage(vehicleId, imageId) { const vehicle = await this.client.get(`/vehicle/${vehicleId}?withGallery=true`); const images = vehicle.gallery || vehicle.images?.GALLERY_ITEM || []; if (images.length === 0) { throw new Error('No images found for this vehicle'); } let imageFound = false; // Update main image status in gallery field images.forEach((img, index) => { const currentId = img.id?.toString() || index.toString(); if (currentId === imageId.toString()) { img.main = true; imageFound = true; } else { img.main = false; } }); if (!imageFound) { throw new Error(`Image with ID ${imageId} not found`); } // Update the correct field based on API response structure if (vehicle.gallery) { vehicle.gallery = images; } else if (vehicle.images?.GALLERY_ITEM) { vehicle.images.GALLERY_ITEM = images; } // Update vehicle await this.client.put(`/vehicle/${vehicleId}`, vehicle); return { vehicleId, mainImageId: imageId, success: true }; } async reorderImages(vehicleId, imageIds) { const vehicle = await this.client.get(`/vehicle/${vehicleId}?withGallery=true`); const images = vehicle.gallery || vehicle.images?.GALLERY_ITEM || []; if (images.length === 0) { throw new Error('No images found for this vehicle'); } // Create a map of current images const imageMap = new Map(); images.forEach((img, index) => { const id = img.id || `image_${index}`; imageMap.set(id, img); }); // Reorder based on provided IDs const reorderedImages = []; imageIds.forEach((id, newPosition) => { const img = imageMap.get(id); if (img) { img.position = newPosition; reorderedImages.push(img); } }); // Add any images not in the reorder list at the end images.forEach((img, index) => { const id = img.id || `image_${index}`; if (!imageIds.includes(id)) { img.position = reorderedImages.length; reorderedImages.push(img); } }); // Update the correct field based on API response structure if (vehicle.gallery) { vehicle.gallery = reorderedImages; } else if (vehicle.images?.GALLERY_ITEM) { vehicle.images.GALLERY_ITEM = reorderedImages; } // Update vehicle await this.client.put(`/vehicle/${vehicleId}`, vehicle); return { vehicleId, reorderedCount: reorderedImages.length, success: true }; } } module.exports = { ImageAPI };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/loukach/stockspark-mcp-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server