This commit is contained in:
johnpccd 2025-05-24 17:23:37 +02:00
parent d894100d4d
commit b3954a2ba1
10 changed files with 475 additions and 438 deletions

View File

@ -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

View File

@ -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">

View File

@ -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">

View File

@ -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}",
]
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 = ""
}

View File

@ -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,

View File

@ -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
View 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()