cp
This commit is contained in:
parent
d894100d4d
commit
b3954a2ba1
@ -124,7 +124,8 @@ class ApiClient {
|
||||
|
||||
// Images API
|
||||
async getImages(page = 1, limit = 20, tags = null) {
|
||||
let endpoint = `/images?page=${page}&limit=${limit}`;
|
||||
const skip = (page - 1) * limit;
|
||||
let endpoint = `/images?skip=${skip}&limit=${limit}`;
|
||||
if (tags) {
|
||||
endpoint += `&tags=${encodeURIComponent(tags)}`;
|
||||
}
|
||||
@ -173,18 +174,6 @@ class ApiClient {
|
||||
const response = await fetch(`${this.baseUrl}/health`);
|
||||
return response.ok;
|
||||
}
|
||||
|
||||
// Get image URL for display
|
||||
getImageUrl(imageId) {
|
||||
this.updateConfig();
|
||||
return `${this.baseUrl}/api/v1/images/${imageId}/file`;
|
||||
}
|
||||
|
||||
// Get image thumbnail URL
|
||||
getThumbnailUrl(imageId, size = 'medium') {
|
||||
this.updateConfig();
|
||||
return `${this.baseUrl}/api/v1/images/${imageId}/thumbnail?size=${size}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Global API client instance
|
||||
|
||||
@ -48,14 +48,14 @@ function displayImages(images) {
|
||||
|
||||
const imagesHtml = images.map(image => `
|
||||
<div class="image-card card">
|
||||
<img src="${apiClient.getThumbnailUrl(image.id)}"
|
||||
<img src="${image.public_url || '/placeholder-image.png'}"
|
||||
alt="${escapeHtml(image.description || 'Image')}"
|
||||
onclick="viewImage('${image.id}')"
|
||||
style="cursor: pointer;">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">${escapeHtml(truncateText(image.description || 'Untitled', 50))}</h6>
|
||||
<p class="card-text small text-muted">
|
||||
<i class="fas fa-calendar me-1"></i>${formatDate(image.created_at)}
|
||||
<i class="fas fa-calendar me-1"></i>${formatDate(image.upload_date)}
|
||||
</p>
|
||||
${image.tags && image.tags.length > 0 ? `
|
||||
<div class="mb-2">
|
||||
@ -212,7 +212,7 @@ async function uploadImage() {
|
||||
formData.append('file', file);
|
||||
formData.append('description', description);
|
||||
if (tags.length > 0) {
|
||||
formData.append('tags', JSON.stringify(tags));
|
||||
formData.append('tags', tags.join(','));
|
||||
}
|
||||
|
||||
await apiClient.uploadImage(formData);
|
||||
@ -238,7 +238,7 @@ async function viewImage(imageId) {
|
||||
|
||||
const modalBody = `
|
||||
<div class="text-center mb-3">
|
||||
<img src="${apiClient.getImageUrl(imageId)}" class="img-fluid rounded"
|
||||
<img src="${image.public_url || '/placeholder-image.png'}" class="img-fluid rounded"
|
||||
style="max-height: 400px;">
|
||||
</div>
|
||||
<div class="row">
|
||||
@ -248,9 +248,9 @@ async function viewImage(imageId) {
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Details</h6>
|
||||
<p><strong>Created:</strong> ${formatDate(image.created_at)}</p>
|
||||
<p><strong>Created:</strong> ${formatDate(image.upload_date)}</p>
|
||||
<p><strong>Size:</strong> ${formatFileSize(image.file_size)}</p>
|
||||
<p><strong>Dimensions:</strong> ${image.width} × ${image.height}</p>
|
||||
<p><strong>Type:</strong> ${image.content_type}</p>
|
||||
</div>
|
||||
</div>
|
||||
${image.tags && image.tags.length > 0 ? `
|
||||
@ -287,7 +287,7 @@ async function editImage(imageId) {
|
||||
const modalBody = `
|
||||
<form id="editImageForm">
|
||||
<div class="mb-3 text-center">
|
||||
<img src="${apiClient.getThumbnailUrl(imageId)}" class="img-fluid rounded"
|
||||
<img src="${image.public_url || '/placeholder-image.png'}" class="img-fluid rounded"
|
||||
style="max-height: 200px;">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
||||
@ -86,7 +86,7 @@ function displaySearchResults(results, query) {
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card search-result h-100">
|
||||
<div class="position-relative">
|
||||
<img src="${apiClient.getThumbnailUrl(result.image.id)}"
|
||||
<img src="${result.image.public_url || '/placeholder-image.png'}"
|
||||
class="card-img-top"
|
||||
alt="${escapeHtml(result.image.description || 'Image')}"
|
||||
style="height: 200px; object-fit: cover; cursor: pointer;"
|
||||
@ -100,7 +100,7 @@ function displaySearchResults(results, query) {
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">${escapeHtml(truncateText(result.image.description || 'Untitled', 60))}</h6>
|
||||
<p class="card-text small text-muted">
|
||||
<i class="fas fa-calendar me-1"></i>${formatDate(result.image.created_at)}
|
||||
<i class="fas fa-calendar me-1"></i>${formatDate(result.image.upload_date)}
|
||||
</p>
|
||||
${result.image.tags && result.image.tags.length > 0 ? `
|
||||
<div class="mb-2">
|
||||
|
||||
@ -1,3 +1,14 @@
|
||||
# Compute default service accounts
|
||||
locals {
|
||||
cloud_run_service_account = var.cloud_run_service_account != "" ? var.cloud_run_service_account : "${data.google_project.current.number}-compute@developer.gserviceaccount.com"
|
||||
cloud_function_service_account = var.cloud_function_service_account != "" ? var.cloud_function_service_account : "${data.google_project.current.number}-compute@developer.gserviceaccount.com"
|
||||
}
|
||||
|
||||
# Get current project information
|
||||
data "google_project" "current" {
|
||||
project_id = var.project_id
|
||||
}
|
||||
|
||||
# Pub/Sub topic for image processing tasks
|
||||
resource "google_pubsub_topic" "image_processing" {
|
||||
name = var.pubsub_topic_name
|
||||
@ -73,7 +84,7 @@ resource "google_pubsub_topic_iam_binding" "image_processing_publisher" {
|
||||
role = "roles/pubsub.publisher"
|
||||
|
||||
members = [
|
||||
"serviceAccount:${var.cloud_run_service_account}",
|
||||
"serviceAccount:${local.cloud_run_service_account}",
|
||||
]
|
||||
}
|
||||
|
||||
@ -83,7 +94,7 @@ resource "google_pubsub_subscription_iam_binding" "image_processing_subscriber"
|
||||
role = "roles/pubsub.subscriber"
|
||||
|
||||
members = [
|
||||
"serviceAccount:${var.cloud_function_service_account}",
|
||||
"serviceAccount:${local.cloud_function_service_account}",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,371 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.10.1",
|
||||
"serial": 28,
|
||||
"serial": 41,
|
||||
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
||||
"outputs": {},
|
||||
"resources": [],
|
||||
"outputs": {
|
||||
"container_registry_url": {
|
||||
"value": "gcr.io/gen-lang-client-0424120530/sereact",
|
||||
"type": "string"
|
||||
},
|
||||
"pubsub_dlq_topic_name": {
|
||||
"value": "image-processing-topic-dlq",
|
||||
"type": "string"
|
||||
},
|
||||
"pubsub_subscription_name": {
|
||||
"value": "image-processing-topic-subscription",
|
||||
"type": "string"
|
||||
},
|
||||
"pubsub_topic_name": {
|
||||
"value": "image-processing-topic",
|
||||
"type": "string"
|
||||
},
|
||||
"storage_bucket_name": {
|
||||
"value": "sereact-storage-bucket",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"mode": "data",
|
||||
"type": "google_project",
|
||||
"name": "current",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"auto_create_network": null,
|
||||
"billing_account": "00CA97-62E5BD-4A62B5",
|
||||
"deletion_policy": "PREVENT",
|
||||
"effective_labels": {
|
||||
"generative-language": "enabled"
|
||||
},
|
||||
"folder_id": null,
|
||||
"id": "projects/gen-lang-client-0424120530",
|
||||
"labels": {
|
||||
"generative-language": "enabled"
|
||||
},
|
||||
"name": "Gemini API",
|
||||
"number": "761163285547",
|
||||
"org_id": null,
|
||||
"project_id": "gen-lang-client-0424120530",
|
||||
"tags": null,
|
||||
"terraform_labels": {
|
||||
"generative-language": "enabled"
|
||||
}
|
||||
},
|
||||
"sensitive_attributes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_project_service",
|
||||
"name": "services",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"index_key": "cloudresourcemanager.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/cloudresourcemanager.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "cloudresourcemanager.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "containerregistry.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/containerregistry.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "containerregistry.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "firestore.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/firestore.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "firestore.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "run.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/run.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "run.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "storage.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/storage.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "storage.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_pubsub_subscription",
|
||||
"name": "image_processing_dlq",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"ack_deadline_seconds": 10,
|
||||
"bigquery_config": [],
|
||||
"cloud_storage_config": [],
|
||||
"dead_letter_policy": [],
|
||||
"effective_labels": {
|
||||
"component": "image-processing-dlq",
|
||||
"environment": "dev",
|
||||
"goog-terraform-provisioned": "true",
|
||||
"service": "sereact"
|
||||
},
|
||||
"enable_exactly_once_delivery": false,
|
||||
"enable_message_ordering": false,
|
||||
"expiration_policy": [
|
||||
{
|
||||
"ttl": "2678400s"
|
||||
}
|
||||
],
|
||||
"filter": "",
|
||||
"id": "projects/gen-lang-client-0424120530/subscriptions/image-processing-topic-dlq-subscription",
|
||||
"labels": {
|
||||
"component": "image-processing-dlq",
|
||||
"environment": "dev",
|
||||
"service": "sereact"
|
||||
},
|
||||
"message_retention_duration": "2592000s",
|
||||
"name": "image-processing-topic-dlq-subscription",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"push_config": [],
|
||||
"retain_acked_messages": true,
|
||||
"retry_policy": [],
|
||||
"terraform_labels": {
|
||||
"component": "image-processing-dlq",
|
||||
"environment": "dev",
|
||||
"goog-terraform-provisioned": "true",
|
||||
"service": "sereact"
|
||||
},
|
||||
"timeouts": null,
|
||||
"topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic-dlq"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19",
|
||||
"dependencies": [
|
||||
"google_pubsub_topic.image_processing_dlq"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_pubsub_topic",
|
||||
"name": "image_processing",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"effective_labels": {
|
||||
"component": "image-processing",
|
||||
"environment": "dev",
|
||||
"goog-terraform-provisioned": "true",
|
||||
"service": "sereact"
|
||||
},
|
||||
"id": "projects/gen-lang-client-0424120530/topics/image-processing-topic",
|
||||
"ingestion_data_source_settings": [],
|
||||
"kms_key_name": "",
|
||||
"labels": {
|
||||
"component": "image-processing",
|
||||
"environment": "dev",
|
||||
"service": "sereact"
|
||||
},
|
||||
"message_retention_duration": "",
|
||||
"message_storage_policy": [],
|
||||
"name": "image-processing-topic",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"schema_settings": [],
|
||||
"terraform_labels": {
|
||||
"component": "image-processing",
|
||||
"environment": "dev",
|
||||
"goog-terraform-provisioned": "true",
|
||||
"service": "sereact"
|
||||
},
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_pubsub_topic",
|
||||
"name": "image_processing_dlq",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"effective_labels": {
|
||||
"component": "image-processing-dlq",
|
||||
"environment": "dev",
|
||||
"goog-terraform-provisioned": "true",
|
||||
"service": "sereact"
|
||||
},
|
||||
"id": "projects/gen-lang-client-0424120530/topics/image-processing-topic-dlq",
|
||||
"ingestion_data_source_settings": [],
|
||||
"kms_key_name": "",
|
||||
"labels": {
|
||||
"component": "image-processing-dlq",
|
||||
"environment": "dev",
|
||||
"service": "sereact"
|
||||
},
|
||||
"message_retention_duration": "",
|
||||
"message_storage_policy": [],
|
||||
"name": "image-processing-topic-dlq",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"schema_settings": [],
|
||||
"terraform_labels": {
|
||||
"component": "image-processing-dlq",
|
||||
"environment": "dev",
|
||||
"goog-terraform-provisioned": "true",
|
||||
"service": "sereact"
|
||||
},
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_pubsub_topic_iam_binding",
|
||||
"name": "image_processing_publisher",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"condition": [],
|
||||
"etag": "BwY14zaElfE=",
|
||||
"id": "projects/gen-lang-client-0424120530/topics/image-processing-topic/roles/pubsub.publisher",
|
||||
"members": [
|
||||
"serviceAccount:761163285547-compute@developer.gserviceaccount.com"
|
||||
],
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"role": "roles/pubsub.publisher",
|
||||
"topic": "projects/gen-lang-client-0424120530/topics/image-processing-topic"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "bnVsbA==",
|
||||
"dependencies": [
|
||||
"data.google_project.current",
|
||||
"google_pubsub_topic.image_processing"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_storage_bucket",
|
||||
"name": "app_bucket",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 3,
|
||||
"attributes": {
|
||||
"autoclass": [],
|
||||
"cors": [],
|
||||
"custom_placement_config": [],
|
||||
"default_event_based_hold": false,
|
||||
"effective_labels": {
|
||||
"goog-terraform-provisioned": "true"
|
||||
},
|
||||
"enable_object_retention": false,
|
||||
"encryption": [],
|
||||
"force_destroy": false,
|
||||
"hierarchical_namespace": [
|
||||
{
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"id": "sereact-storage-bucket",
|
||||
"labels": null,
|
||||
"lifecycle_rule": [],
|
||||
"location": "US-CENTRAL1",
|
||||
"logging": [],
|
||||
"name": "sereact-storage-bucket",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"project_number": 761163285547,
|
||||
"public_access_prevention": "inherited",
|
||||
"requester_pays": false,
|
||||
"retention_policy": [],
|
||||
"rpo": null,
|
||||
"self_link": "https://www.googleapis.com/storage/v1/b/sereact-storage-bucket",
|
||||
"soft_delete_policy": [
|
||||
{
|
||||
"effective_time": "2025-05-24T15:17:40.181Z",
|
||||
"retention_duration_seconds": 604800
|
||||
}
|
||||
],
|
||||
"storage_class": "STANDARD",
|
||||
"terraform_labels": {
|
||||
"goog-terraform-provisioned": "true"
|
||||
},
|
||||
"time_created": "2025-05-24T15:17:40.181Z",
|
||||
"timeouts": null,
|
||||
"uniform_bucket_level_access": true,
|
||||
"updated": "2025-05-24T15:17:40.181Z",
|
||||
"url": "gs://sereact-storage-bucket",
|
||||
"versioning": [],
|
||||
"website": []
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsInJlYWQiOjI0MDAwMDAwMDAwMCwidXBkYXRlIjoyNDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjMifQ==",
|
||||
"dependencies": [
|
||||
"google_project_service.services"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"check_results": null
|
||||
}
|
||||
|
||||
@ -1,416 +1,9 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.10.1",
|
||||
"serial": 18,
|
||||
"serial": 28,
|
||||
"lineage": "a183cd95-f987-8698-c6dd-84e933c394a5",
|
||||
"outputs": {
|
||||
"cloud_run_url": {
|
||||
"value": "https://sereact-p64zpdtkta-uc.a.run.app",
|
||||
"type": "string"
|
||||
},
|
||||
"container_registry_url": {
|
||||
"value": "gcr.io/gen-lang-client-0424120530/sereact",
|
||||
"type": "string"
|
||||
},
|
||||
"firestore_database_id": {
|
||||
"value": "projects/gen-lang-client-0424120530/databases/imagedb",
|
||||
"type": "string"
|
||||
},
|
||||
"storage_bucket_name": {
|
||||
"value": "sereact-storage-bucket",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_cloud_run_service",
|
||||
"name": "sereact",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 2,
|
||||
"attributes": {
|
||||
"autogenerate_revision_name": false,
|
||||
"id": "locations/us-central1/namespaces/gen-lang-client-0424120530/services/sereact",
|
||||
"location": "us-central1",
|
||||
"metadata": [
|
||||
{
|
||||
"annotations": {},
|
||||
"effective_annotations": {
|
||||
"run.googleapis.com/ingress": "all",
|
||||
"run.googleapis.com/ingress-status": "all",
|
||||
"run.googleapis.com/operation-id": "0982194f-b3e8-45b8-a33f-7ed5fd529307",
|
||||
"run.googleapis.com/urls": "[\"https://sereact-761163285547.us-central1.run.app\",\"https://sereact-p64zpdtkta-uc.a.run.app\"]",
|
||||
"serving.knative.dev/creator": "johnpccd3@gmail.com",
|
||||
"serving.knative.dev/lastModifier": "johnpccd3@gmail.com"
|
||||
},
|
||||
"effective_labels": {
|
||||
"cloud.googleapis.com/location": "us-central1"
|
||||
},
|
||||
"generation": 1,
|
||||
"labels": {},
|
||||
"namespace": "gen-lang-client-0424120530",
|
||||
"resource_version": "AAY108GXDwM",
|
||||
"self_link": "/apis/serving.knative.dev/v1/namespaces/761163285547/services/sereact",
|
||||
"terraform_labels": {},
|
||||
"uid": "5b695749-5095-4fb5-86bd-4d86f77dc7da"
|
||||
}
|
||||
],
|
||||
"name": "sereact",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"status": [
|
||||
{
|
||||
"conditions": [
|
||||
{
|
||||
"message": "",
|
||||
"reason": "",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"reason": "",
|
||||
"status": "True",
|
||||
"type": "ConfigurationsReady"
|
||||
},
|
||||
{
|
||||
"message": "",
|
||||
"reason": "",
|
||||
"status": "True",
|
||||
"type": "RoutesReady"
|
||||
}
|
||||
],
|
||||
"latest_created_revision_name": "sereact-00001-bt2",
|
||||
"latest_ready_revision_name": "sereact-00001-bt2",
|
||||
"observed_generation": 1,
|
||||
"traffic": [
|
||||
{
|
||||
"latest_revision": true,
|
||||
"percent": 100,
|
||||
"revision_name": "sereact-00001-bt2",
|
||||
"tag": "",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
"url": "https://sereact-p64zpdtkta-uc.a.run.app"
|
||||
}
|
||||
],
|
||||
"template": [
|
||||
{
|
||||
"metadata": [
|
||||
{
|
||||
"annotations": {
|
||||
"autoscaling.knative.dev/maxScale": "3"
|
||||
},
|
||||
"generation": 0,
|
||||
"labels": {
|
||||
"run.googleapis.com/startupProbeType": "Default"
|
||||
},
|
||||
"name": "",
|
||||
"namespace": "",
|
||||
"resource_version": "",
|
||||
"self_link": "",
|
||||
"uid": ""
|
||||
}
|
||||
],
|
||||
"spec": [
|
||||
{
|
||||
"container_concurrency": 80,
|
||||
"containers": [
|
||||
{
|
||||
"args": null,
|
||||
"command": null,
|
||||
"env": [
|
||||
{
|
||||
"name": "DATABASE_NAME",
|
||||
"value": "imagedb",
|
||||
"value_from": []
|
||||
},
|
||||
{
|
||||
"name": "GCS_BUCKET_NAME",
|
||||
"value": "sereact-storage-bucket",
|
||||
"value_from": []
|
||||
},
|
||||
{
|
||||
"name": "LOG_LEVEL",
|
||||
"value": "INFO",
|
||||
"value_from": []
|
||||
}
|
||||
],
|
||||
"env_from": [],
|
||||
"image": "gcr.io/google-samples/hello-app:1.0",
|
||||
"liveness_probe": [],
|
||||
"name": "",
|
||||
"ports": [
|
||||
{
|
||||
"container_port": 8080,
|
||||
"name": "http1",
|
||||
"protocol": ""
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"limits": {
|
||||
"cpu": "1",
|
||||
"memory": "512Mi"
|
||||
},
|
||||
"requests": null
|
||||
}
|
||||
],
|
||||
"startup_probe": [
|
||||
{
|
||||
"failure_threshold": 1,
|
||||
"grpc": [],
|
||||
"http_get": [],
|
||||
"initial_delay_seconds": 0,
|
||||
"period_seconds": 240,
|
||||
"tcp_socket": [
|
||||
{
|
||||
"port": 8080
|
||||
}
|
||||
],
|
||||
"timeout_seconds": 240
|
||||
}
|
||||
],
|
||||
"volume_mounts": [],
|
||||
"working_dir": ""
|
||||
}
|
||||
],
|
||||
"node_selector": null,
|
||||
"service_account_name": "761163285547-compute@developer.gserviceaccount.com",
|
||||
"serving_state": "",
|
||||
"timeout_seconds": 300,
|
||||
"volumes": []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeouts": null,
|
||||
"traffic": [
|
||||
{
|
||||
"latest_revision": true,
|
||||
"percent": 100,
|
||||
"revision_name": "",
|
||||
"tag": "",
|
||||
"url": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH0sInNjaGVtYV92ZXJzaW9uIjoiMiJ9",
|
||||
"dependencies": [
|
||||
"google_project_service.services"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_cloud_run_service_iam_member",
|
||||
"name": "public_access",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"condition": [],
|
||||
"etag": "BwY108GyUf0=",
|
||||
"id": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact/roles/run.invoker/allUsers",
|
||||
"location": "us-central1",
|
||||
"member": "allUsers",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"role": "roles/run.invoker",
|
||||
"service": "v1/projects/gen-lang-client-0424120530/locations/us-central1/services/sereact"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "bnVsbA==",
|
||||
"dependencies": [
|
||||
"google_cloud_run_service.sereact",
|
||||
"google_project_service.services"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_firestore_database",
|
||||
"name": "database",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"app_engine_integration_mode": "DISABLED",
|
||||
"cmek_config": [],
|
||||
"concurrency_mode": "PESSIMISTIC",
|
||||
"create_time": "",
|
||||
"database_edition": "STANDARD",
|
||||
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
|
||||
"deletion_policy": "ABANDON",
|
||||
"earliest_version_time": "2025-05-23T20:47:31.801717Z",
|
||||
"etag": "IIelp4e8uo0DMPX0nai7uo0D",
|
||||
"id": "projects/gen-lang-client-0424120530/databases/imagedb",
|
||||
"key_prefix": "",
|
||||
"location_id": "us-central1",
|
||||
"name": "imagedb",
|
||||
"point_in_time_recovery_enablement": "POINT_IN_TIME_RECOVERY_DISABLED",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"timeouts": null,
|
||||
"type": "FIRESTORE_NATIVE",
|
||||
"uid": "177e30f2-9b67-428c-9979-22e625b929c4",
|
||||
"update_time": "",
|
||||
"version_retention_period": "3600s"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19",
|
||||
"dependencies": [
|
||||
"google_project_service.services"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_project_service",
|
||||
"name": "services",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"index_key": "cloudresourcemanager.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/cloudresourcemanager.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "cloudresourcemanager.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "containerregistry.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/containerregistry.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "containerregistry.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "firestore.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/firestore.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "firestore.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "run.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/run.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "run.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
},
|
||||
{
|
||||
"index_key": "storage.googleapis.com",
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"disable_dependent_services": null,
|
||||
"disable_on_destroy": false,
|
||||
"id": "gen-lang-client-0424120530/storage.googleapis.com",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"service": "storage.googleapis.com",
|
||||
"timeouts": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjEyMDAwMDAwMDAwMDAsInJlYWQiOjYwMDAwMDAwMDAwMCwidXBkYXRlIjoxMjAwMDAwMDAwMDAwfX0="
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "google_storage_bucket",
|
||||
"name": "app_bucket",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 3,
|
||||
"attributes": {
|
||||
"autoclass": [],
|
||||
"cors": [],
|
||||
"custom_placement_config": [],
|
||||
"default_event_based_hold": false,
|
||||
"effective_labels": {
|
||||
"goog-terraform-provisioned": "true"
|
||||
},
|
||||
"enable_object_retention": false,
|
||||
"encryption": [],
|
||||
"force_destroy": false,
|
||||
"hierarchical_namespace": [
|
||||
{
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"id": "sereact-storage-bucket",
|
||||
"labels": {},
|
||||
"lifecycle_rule": [],
|
||||
"location": "US-CENTRAL1",
|
||||
"logging": [],
|
||||
"name": "sereact-storage-bucket",
|
||||
"project": "gen-lang-client-0424120530",
|
||||
"project_number": 761163285547,
|
||||
"public_access_prevention": "inherited",
|
||||
"requester_pays": false,
|
||||
"retention_policy": [],
|
||||
"rpo": null,
|
||||
"self_link": "https://www.googleapis.com/storage/v1/b/sereact-storage-bucket",
|
||||
"soft_delete_policy": [
|
||||
{
|
||||
"effective_time": "2025-05-23T20:47:26.788Z",
|
||||
"retention_duration_seconds": 604800
|
||||
}
|
||||
],
|
||||
"storage_class": "STANDARD",
|
||||
"terraform_labels": {
|
||||
"goog-terraform-provisioned": "true"
|
||||
},
|
||||
"time_created": "2025-05-23T20:47:26.788Z",
|
||||
"timeouts": null,
|
||||
"uniform_bucket_level_access": true,
|
||||
"updated": "2025-05-23T20:47:26.788Z",
|
||||
"url": "gs://sereact-storage-bucket",
|
||||
"versioning": [],
|
||||
"website": []
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsInJlYWQiOjI0MDAwMDAwMDAwMCwidXBkYXRlIjoyNDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjMifQ==",
|
||||
"dependencies": [
|
||||
"google_project_service.services"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": {},
|
||||
"resources": [],
|
||||
"check_results": null
|
||||
}
|
||||
|
||||
@ -41,9 +41,11 @@ variable "pubsub_topic_name" {
|
||||
variable "cloud_run_service_account" {
|
||||
description = "The service account email for Cloud Run"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "cloud_function_service_account" {
|
||||
description = "The service account email for Cloud Functions"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
@ -62,6 +62,9 @@ async def upload_image(
|
||||
file, str(current_user.team_id)
|
||||
)
|
||||
|
||||
# Generate public URL
|
||||
public_url = storage_service.generate_public_url(storage_path)
|
||||
|
||||
# Process tags
|
||||
tag_list = []
|
||||
if tags:
|
||||
@ -74,6 +77,7 @@ async def upload_image(
|
||||
file_size=file_size,
|
||||
content_type=content_type,
|
||||
storage_path=storage_path,
|
||||
public_url=public_url,
|
||||
team_id=current_user.team_id,
|
||||
uploader_id=current_user.id,
|
||||
description=description,
|
||||
@ -105,6 +109,7 @@ async def upload_image(
|
||||
file_size=created_image.file_size,
|
||||
content_type=created_image.content_type,
|
||||
storage_path=created_image.storage_path,
|
||||
public_url=created_image.public_url,
|
||||
team_id=str(created_image.team_id),
|
||||
uploader_id=str(created_image.uploader_id),
|
||||
upload_date=created_image.upload_date,
|
||||
@ -163,6 +168,19 @@ async def list_images(
|
||||
# Convert to response
|
||||
response_images = []
|
||||
for image in images:
|
||||
# Generate public URL if not set
|
||||
public_url = image.public_url
|
||||
if not public_url and image.storage_path:
|
||||
# Make the file public and generate URL
|
||||
try:
|
||||
storage_service.make_file_public(image.storage_path)
|
||||
public_url = storage_service.generate_public_url(image.storage_path)
|
||||
# Update the database with the public URL
|
||||
await image_repository.update(image.id, {"public_url": public_url})
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to make file public or update URL for image {image.id}: {e}")
|
||||
public_url = storage_service.generate_public_url(image.storage_path)
|
||||
|
||||
response_images.append(ImageResponse(
|
||||
id=str(image.id),
|
||||
filename=image.filename,
|
||||
@ -170,6 +188,7 @@ async def list_images(
|
||||
file_size=image.file_size,
|
||||
content_type=image.content_type,
|
||||
storage_path=image.storage_path,
|
||||
public_url=public_url,
|
||||
team_id=str(image.team_id),
|
||||
uploader_id=str(image.uploader_id),
|
||||
upload_date=image.upload_date,
|
||||
@ -214,6 +233,19 @@ async def get_image(
|
||||
# Update last accessed
|
||||
await image_repository.update_last_accessed(obj_id)
|
||||
|
||||
# Generate public URL if not set
|
||||
public_url = image.public_url
|
||||
if not public_url and image.storage_path:
|
||||
# Make the file public and generate URL
|
||||
try:
|
||||
storage_service.make_file_public(image.storage_path)
|
||||
public_url = storage_service.generate_public_url(image.storage_path)
|
||||
# Update the database with the public URL
|
||||
await image_repository.update(image.id, {"public_url": public_url})
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to make file public or update URL for image {image.id}: {e}")
|
||||
public_url = storage_service.generate_public_url(image.storage_path)
|
||||
|
||||
# Convert to response
|
||||
response = ImageResponse(
|
||||
id=str(image.id),
|
||||
@ -222,6 +254,7 @@ async def get_image(
|
||||
file_size=image.file_size,
|
||||
content_type=image.content_type,
|
||||
storage_path=image.storage_path,
|
||||
public_url=public_url,
|
||||
team_id=str(image.team_id),
|
||||
uploader_id=str(image.uploader_id),
|
||||
upload_date=image.upload_date,
|
||||
|
||||
@ -122,7 +122,10 @@ class StorageService:
|
||||
# Upload the file
|
||||
blob.upload_from_string(content, content_type=content_type)
|
||||
|
||||
logger.info(f"File uploaded: {storage_path}")
|
||||
# Make the blob publicly readable
|
||||
blob.make_public()
|
||||
|
||||
logger.info(f"File uploaded and made public: {storage_path}")
|
||||
|
||||
# Seek back to the beginning for future reads
|
||||
await file.seek(0)
|
||||
@ -222,6 +225,29 @@ class StorageService:
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating signed URL: {e}")
|
||||
raise
|
||||
|
||||
def make_file_public(self, storage_path: str) -> bool:
|
||||
"""
|
||||
Make a file publicly accessible
|
||||
|
||||
Args:
|
||||
storage_path: Storage path of the file
|
||||
|
||||
Returns:
|
||||
True if file was made public, False if not found
|
||||
"""
|
||||
try:
|
||||
blob = self.bucket.blob(storage_path)
|
||||
if not blob.exists():
|
||||
logger.warning(f"File not found for making public: {storage_path}")
|
||||
return False
|
||||
|
||||
blob.make_public()
|
||||
logger.info(f"File made public: {storage_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error making file public: {e}")
|
||||
raise
|
||||
|
||||
# Create a singleton service
|
||||
storage_service = StorageService()
|
||||
21
test_images_api.py
Normal file
21
test_images_api.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Test the images API
|
||||
def test_images_api():
|
||||
base_url = "http://localhost:8000"
|
||||
|
||||
# First, let's check if we need to bootstrap or if there are existing API keys
|
||||
try:
|
||||
# Try to get images without API key first to see the error
|
||||
response = requests.get(f"{base_url}/api/v1/images")
|
||||
print(f"Images API without auth: {response.status_code}")
|
||||
print(f"Response: {response.text[:200]}...")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error testing images API: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_images_api()
|
||||
Loading…
x
Reference in New Issue
Block a user