validops-east-1 commited on
Commit
efa9f90
·
1 Parent(s): 5f78436
app/api/v1/gcs.py CHANGED
@@ -2,9 +2,12 @@ from __future__ import annotations
2
 
3
  import base64
4
  import json
 
5
  import re
6
  import time
 
7
  from typing import Any, Dict, Optional
 
8
 
9
  from fastapi import APIRouter, Depends, File, Form, Header, HTTPException, Query, Response, UploadFile
10
 
@@ -441,10 +444,10 @@ async def list_objects(
441
  summary="Upload an object from a file or a URL (up to the server upload limit)")
442
  async def upload_object(
443
  bucket: str,
444
- name: str = Form(..., min_length=1, max_length=1024, description="Destination object name"),
445
  file: Optional[UploadFile] = File(None, description="Object content to upload"),
446
  file_url: Optional[str] = Form(None, description="Publicly reachable URL whose content becomes the object"),
447
- content_type: str = Form("application/octet-stream", description="Content-Type of the uploaded object"),
448
  metadata_json: str = Form(None, description="Optional JSON object of object metadata to set on upload"),
449
  if_generation_match: Optional[int] = Form(None, description="Generation match condition"),
450
  credentials_file: Optional[UploadFile] = File(None, description="Service account JSON file (alternative to inline credentials)"),
@@ -458,12 +461,27 @@ async def upload_object(
458
  raise HTTPException(status_code=422, detail="Either 'file' or 'file_url' must be provided to upload an object.")
459
  if file is not None and file_url:
460
  raise HTTPException(status_code=422, detail="Provide either 'file' or 'file_url', not both.")
461
- if not re.fullmatch(r"[\w./+-]{1,255}", content_type):
462
- raise HTTPException(status_code=422, detail="content_type must be a valid media type.")
 
 
 
 
 
 
 
463
  cleaned_name = name.strip()
464
- if not cleaned_name or cleaned_name.startswith("/"):
465
  raise HTTPException(status_code=422, detail="Object name must be non-empty and must not start with '/'.")
466
 
 
 
 
 
 
 
 
 
467
  if credentials_file is not None:
468
  cred_bytes = await _read_file_limited(credentials_file, what="credentials file")
469
  creds = await _resolve(service, file_bytes=cred_bytes, file_name=credentials_file.filename)
@@ -477,8 +495,8 @@ async def upload_object(
477
  raise HTTPException(status_code=exc.status_code, detail=exc.message)
478
  if len(content) > _MAX_UPLOAD_BYTES:
479
  raise HTTPException(status_code=413, detail=f"Content from file_url exceeds the {_MAX_UPLOAD_BYTES} byte upload limit")
480
- if content_type == "application/octet-stream":
481
- content_type = fetched_type or content_type
482
  else:
483
  content = await _read_file_limited(file, what="file")
484
 
 
2
 
3
  import base64
4
  import json
5
+ import mimetypes
6
  import re
7
  import time
8
+ from pathlib import Path
9
  from typing import Any, Dict, Optional
10
+ from urllib.parse import urlparse
11
 
12
  from fastapi import APIRouter, Depends, File, Form, Header, HTTPException, Query, Response, UploadFile
13
 
 
444
  summary="Upload an object from a file or a URL (up to the server upload limit)")
445
  async def upload_object(
446
  bucket: str,
447
+ name: Optional[str] = Form(None, min_length=1, max_length=1024, description="Destination object name (defaults to the uploaded file name)"),
448
  file: Optional[UploadFile] = File(None, description="Object content to upload"),
449
  file_url: Optional[str] = Form(None, description="Publicly reachable URL whose content becomes the object"),
450
+ content_type: Optional[str] = Form(None, description="Content-Type of the uploaded object (auto-detected when omitted)"),
451
  metadata_json: str = Form(None, description="Optional JSON object of object metadata to set on upload"),
452
  if_generation_match: Optional[int] = Form(None, description="Generation match condition"),
453
  credentials_file: Optional[UploadFile] = File(None, description="Service account JSON file (alternative to inline credentials)"),
 
461
  raise HTTPException(status_code=422, detail="Either 'file' or 'file_url' must be provided to upload an object.")
462
  if file is not None and file_url:
463
  raise HTTPException(status_code=422, detail="Provide either 'file' or 'file_url', not both.")
464
+
465
+ if name is None:
466
+ if file is not None and file.filename:
467
+ name = Path(file.filename).name
468
+ elif file_url:
469
+ parsed_url = urlparse(file_url)
470
+ name = Path(parsed_url.path).name or "download"
471
+ if not name or not name.strip():
472
+ raise HTTPException(status_code=422, detail="Object name must be non-empty and must not start with '/'.")
473
  cleaned_name = name.strip()
474
+ if cleaned_name.startswith("/"):
475
  raise HTTPException(status_code=422, detail="Object name must be non-empty and must not start with '/'.")
476
 
477
+ if content_type is None:
478
+ if file is not None and file.content_type:
479
+ content_type = file.content_type
480
+ else:
481
+ content_type = mimetypes.guess_type(cleaned_name)[0] or "application/octet-stream"
482
+ if not re.fullmatch(r"[\w./+-]{1,255}", content_type):
483
+ raise HTTPException(status_code=422, detail="content_type must be a valid media type.")
484
+
485
  if credentials_file is not None:
486
  cred_bytes = await _read_file_limited(credentials_file, what="credentials file")
487
  creds = await _resolve(service, file_bytes=cred_bytes, file_name=credentials_file.filename)
 
495
  raise HTTPException(status_code=exc.status_code, detail=exc.message)
496
  if len(content) > _MAX_UPLOAD_BYTES:
497
  raise HTTPException(status_code=413, detail=f"Content from file_url exceeds the {_MAX_UPLOAD_BYTES} byte upload limit")
498
+ if fetched_type and (content_type == "application/octet-stream" or content_type == mimetypes.guess_type(cleaned_name)[0]):
499
+ content_type = fetched_type
500
  else:
501
  content = await _read_file_limited(file, what="file")
502
 
app/services/gcs_service.py CHANGED
@@ -515,7 +515,7 @@ class GCSService:
515
  async def test_bucket_permissions(
516
  self, creds: GCSCredentials, bucket: str, permissions: List[str],
517
  ) -> Dict[str, Any]:
518
- params = {"permissions": ",".join(permissions)}
519
  return await self._request("GET", f"/b/{quote(bucket, safe='')}/iam/testPermissions", creds, params=params)
520
 
521
  # ------------------------------------------------------------------
 
515
  async def test_bucket_permissions(
516
  self, creds: GCSCredentials, bucket: str, permissions: List[str],
517
  ) -> Dict[str, Any]:
518
+ params = {"permissions": permissions}
519
  return await self._request("GET", f"/b/{quote(bucket, safe='')}/iam/testPermissions", creds, params=params)
520
 
521
  # ------------------------------------------------------------------
app/services/media_conversion_service.py CHANGED
@@ -308,6 +308,12 @@ def _convert_image_bytes(data: bytes, params: ImageConversionParams, out_path: P
308
  img = img.resize(target, Image.LANCZOS)
309
 
310
  fmt = _fmt_str(params.format)
 
 
 
 
 
 
311
  if fmt == "JPEG":
312
  img = _normalise_for_jpeg(img)
313
  elif fmt in ("PNG", "WEBP") and img.mode not in ("RGB", "RGBA", "L", "LA"):
 
308
  img = img.resize(target, Image.LANCZOS)
309
 
310
  fmt = _fmt_str(params.format)
311
+ if detected == fmt:
312
+ raise MediaConversionError(
313
+ f"Input is already {fmt}. Same-format conversion is not supported; "
314
+ "choose a different output format.",
315
+ status_code=422,
316
+ )
317
  if fmt == "JPEG":
318
  img = _normalise_for_jpeg(img)
319
  elif fmt in ("PNG", "WEBP") and img.mode not in ("RGB", "RGBA", "L", "LA"):