docs
/
Courses — AppEngine

AppEngine File Storage APIs: Uploads, Private Files and Downloads

Watch on YouTube ↗

Video chapters
  1. AppEngine File Storage APIs
  2. Stored files and record references
  3. Prepare the organization and staff session
  4. Upload a file with multipart form data
  5. Understand paths, links and image previews
  6. Attach the uploaded image to a product
  7. Inspect the file and product in Appmint Studio
  8. List files, folders and pages
  9. Check existence, metadata and file content
  10. Upload a private document
  11. Create an expiring download link
  12. Change access and check the actual URL
  13. Upload, list and delete as a customer
  14. Copy and move files
  15. Replace an attachment without breaking its reference
  16. Import an existing image from a URL
  17. Folders, thumbnails and the file index
  18. Remove files and check remaining references
  19. Additional operations, permissions and limits
  20. Use file storage in your application

Written guide

Companion source code

Get the course source code on GitHub. The folder includes the supplied examples and setup instructions. Keep credentials in your own environment; use the maintained API reference when adapting recorded request examples.

Outcome: upload images and documents, attach a file to a built-in product, inspect stored objects, issue an expiring private download, use customer file routes, and manage replacement and cleanup without losing the record-to-file relationship.

This course teaches the AppEngine storage APIs. Appmint Studio is the real client used to inspect the same results. The examples use one botanical image and harmless training documents; you can substitute your own files and organization. Current API results take precedence over older field descriptions. The evidence section distinguishes verified behavior from remaining production checks.

A stored file is an object: its bytes live at a storage path. A product, customer document or page can reference that object without containing the image or document bytes itself. A URL is how a client opens the file. A signed URL additionally carries time-limited authorization; the path remains the identifier you use to request a new link.

Uploading an image and attaching it to a product are separate actions. Removing a product reference does not by itself delete the stored file. Moving or deleting a file does not automatically repair records that still reference its previous path. This distinction explains the sequence used throughout the course: upload, inspect, attach, read back, then manage access or replacement.

Built-in records already expose file fields. For example, sf_product.data.images is an array of file references. A minimal public image reference is:

{
  "path": "YOUR_ORG/course-files-1009/cedar-botanical-print.png",
  "url": "THE_PUBLIC_URL_RETURNED_BY_APPENGINE"
}

Use the actual returned values. YOUR_ORG and the uppercase URL above are substitution markers, not literal working addresses. The FileInfo schema also supports content type and metadata. Upload-response preview links and a record's FileInfo structure are not interchangeable; map the fields you need rather than copying an entire upload response into every attachment field.

AppEngine also has a fileinfo collection for indexed file references. A stored object, an indexed fileinfo record, and a product's image reference are three distinct things. You do not need to create a custom collection merely to upload a file. If your application needs a new kind of document-owning record, follow Database & Repository APIs: custom collection setup.

2. Prepare the organization, identity and files

Obtain API, ORG and STAFF_TOKEN from Database & Repository APIs: set up what you need. That section shows the actual staff sign-in route, challenge handling and returned token. API Foundations supplies the organization setup; it is not a substitute for the staff-session procedure.

Each staff request below sends orgid: ORG and Authorization: Bearer STAFF_TOKEN. Use the AppEngine address provided for your environment. Appmint Studio is available at studio.appmint.io; its website address and your API address serve different purposes.

The staff role needs the relevant content permission:

OperationCurrent permission
Upload, URL import, folder creation, thumbnail generationCreate
List, exists, stat, read, signed URL, file index, driver metadataRead
Copy, move, append/prepend, visibility changeUpdate
DeleteDelete

Use Roles and Permissions if those permissions need assigning. A read-permission operation can still have side effects: the current index endpoint writes index records, and favicon generation writes files. Choose operations deliberately.

The organization must have working storage. On an operator-managed deployment the source uses S3 endpoint, region, bucket and credentials on the server; these never belong in a browser upload example. Public/private behavior depends on the configured provider and must be checked against the resulting object URLs. The existing Platform and Deployment course covers application deployment, not a complete S3 administrator setup.

Prepare cedar-botanical-print.png, supplier-notes.txt and supplier-sheet.pdf. Use a dedicated folder such as course-files-1009. The course product is Cedar Botanical Print — File API course, SKU FILES-CEDAR-1009. Reuse your own saved product or create a draft product in Storefront → Product before attaching an image; do not select an unrelated live product. The lab created a draft sf_product record with name, sku, description, status, price and an initially empty images array. The Headless Storefront course covers the broader product lifecycle and publication.

3. Upload an image with multipart form data

Call POST /repository/file/upload. This route expects an actual multipart file field named file, not a base64 string or JSON property. location is the destination folder. The uploaded file's original name becomes the final name. isPrivate is a multipart text field: the exact string true selects private upload; false makes this example public.

TypeScript and JavaScript in a browser

The same FormData operation works in both languages. TypeScript can type the selected file as File; plain JavaScript omits that annotation. The file comes from your actual file picker.

async function uploadImage(file: File, API: string, ORG: string, token: string) {
  const form = new FormData();
  form.append("file", file);
  form.append("location", "course-files-1009");
  form.append("isPrivate", "false");
  const response = await fetch(`${API}/repository/file/upload`, {
    method: "POST",
    headers: { orgid: ORG, Authorization: `Bearer ${token}` },
    body: form
  });
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}
const form = new FormData();
form.append("file", document.querySelector('input[type="file"]').files[0]);
form.append("location", "course-files-1009");
form.append("isPrivate", "false");
const response = await fetch(`${API}/repository/file/upload`, {
  method: "POST",
  headers: { orgid: ORG, Authorization: `Bearer ${STAFF_TOKEN}` },
  body: form
});
if (!response.ok) throw new Error(await response.text());
const uploaded = await response.json();

Python

import requests
with open("cedar-botanical-print.png", "rb") as image:
    response = requests.post(
        f"{API}/repository/file/upload",
        headers={"orgid": ORG, "Authorization": f"Bearer {STAFF_TOKEN}"},
        files={"file": ("cedar-botanical-print.png", image, "image/png")},
        data={"location": "course-files-1009", "isPrivate": "false"},
        timeout=60,
    )
response.raise_for_status()
uploaded = response.json()
print(uploaded)

curl

curl --fail-with-body -sS -X POST "$API/repository/file/upload" \
  -H "orgid: $ORG" \
  -H "Authorization: Bearer $STAFF_TOKEN" \
  -F '[email protected];type=image/png' \
  -F 'location=course-files-1009' \
  -F 'isPrivate=false'

Postman

Choose POST and the same route. Add orgid and bearer authorization. Choose Body → form-data. Set file to type File and select the image. Add text rows location=course-files-1009 and isPrivate=false. Let Postman set the multipart Content-Type and boundary.

JSON field map

This describes the multipart fields; it is not a JSON body accepted by the upload route.

{
  "file": "binary file selected from your device",
  "location": "course-files-1009",
  "isPrivate": "false"
}

Do not manually set Content-Type: application/json on these upload examples. The client must supply the multipart boundary. A missing file field is a different problem from a rejected token or full storage allowance.

Actual File Manager: course folder and uploaded botanical image

Actual Appmint Studio capture after selecting the saved course folder. The API object is visible in its real file library.

4. Read the path, opening URL and previews

The executed PNG upload returned HTTP 201, with raw, path, signedUrl, xs, sm and md. The original contained 52,048 bytes. A fresh anonymous request to its public URL returned the same image bytes.

{
  "path": "YOUR_ORG/course-files-1009/cedar-botanical-print.png",
  "signedUrl": "PUBLIC_IMAGE_URL",
  "raw": { "Size": 52048, "Key": "YOUR_ORG/course-files-1009/cedar-botanical-print.png" },
  "xs": "RETURNED_EXTRA_SMALL_PREVIEW_URL",
  "sm": "RETURNED_SMALL_PREVIEW_URL",
  "md": "RETURNED_MEDIUM_PREVIEW_URL"
}

This shortened shape omits storage bookkeeping and replaces actual host/signature values with readable markers. Keep the complete real response in your application. signedUrl is a field name, not proof that a file is private: the public original's value had no signing query, while its previews had signed links. Private text and PDF uploads did not produce image preview keys.

Supported extensions and preview widths are server-configurable. The current defaults include PNG, JPEG/JPG, GIF, TIFF, BMP and WebP, with widths 100, 200 and 400. Do not promise those exact settings for every installation. Image processing can fail independently of the original upload, so open the actual returned previews before displaying them as successful generated results.

The file/url operation returns a string URL; it does not check existence or grant private access. A syntactically valid URL is not evidence that the object is present or publicly readable.

5. Attach the file to a product and read it back

Find the intended product, rather than guessing its ID:

{
  "query": { "data.sku": "FILES-CEDAR-1009" },
  "options": { "page": 1, "pageSize": 5 }
}

Send that body to POST /repository/find/sf_product. Inspect the list, choose the exact matching record and keep its sk as PRODUCT_ID. Read its existing data.images before replacing the list.

Send this body to POST /repository/update-partial/sf_product/PRODUCT_ID, substituting the upload's returned path and public URL:

{
  "data.images": [
    {
      "path": "YOUR_ORG/course-files-1009/cedar-botanical-print.png",
      "url": "THE_PUBLIC_IMAGE_URL"
    }
  ]
}

The dotted key targets data.images. It replaces the entire array; retain existing entries when adding another image. The controlled update returned HTTP 201 and true. Then GET /repository/get/sf_product/PRODUCT_ID returned the same saved path and URL inside data.images. This readback establishes the attachment; the upload response alone does not.

For a private document, retain its path and generate a fresh download link when authorized. Do not treat a temporary signed URL saved in a record as a permanent download address.

6. Inspect the result in Appmint Studio

File Manager is the actual file-management client; its route is /document. Locate the course folder and open the botanical image. Compare its filename and path to the upload response. Open the same saved product in Storefront → Product and inspect its image field. File Manager establishes which object exists; the product field establishes which object the record uses.

Current navigation is Digital Asset Management → Upload files → Maximize → course-files-1009. The product stores the attachment in its gallery; this is separate from its default image. Keep dialogs large enough to read the whole field. Do not publish the draft product merely to inspect its stored attachment.

Actual product gallery with the botanical image attached

Actual product media gallery after the API attachment. The default image is a separate field.

7. List files, folders and pages

Call POST /repository/file/flatlist with:

{
  "prefix": "course-files-1009/",
  "pageNumber": 1,
  "maxKeys": 20,
  "recursive": true,
  "checkPrivacy": true
}

The verified result has files, hasMore, startPage, prefix and pageNumber. Each actual file supplies its storage metadata and path; links depend on access and calling identity. In this staff response the image was labelled public and both training documents private. Their returned paths matched the uploads.

Use recursive:false for a folder-by-folder browser, and distinguish folder entries from files. Read hasMore before requesting another page. Do not confuse an empty filtered result with a missing upload. Privacy inspection performs additional checks per object; request it when your interface needs that information.

The provider also accepts filters such as endsWith, includes, thumbnail and foldersOnly. Check their returned entries on your prefix rather than assuming they behave like a Repository query. The file listing is an object-storage operation; Repository/find/fileinfo searches separately indexed records.

8. Check existence, metadata and contents

All three operations take the saved location:

RouteAdditional bodyResult to inspect
POST /repository/file/existsNoneexists boolean; storage metadata in raw
POST /repository/file/statNonesize, modified, raw
POST /repository/file"encoding":"utf8" for textcontent and raw
{
  "location": "YOUR_ORG/course-files-1009/private/supplier-notes.txt",
  "encoding": "utf8"
}

The text read returned the actual course notes, not a URL. The image stat returned 52,048 bytes and MIME image/png; exists returned true. A correct size or filename is useful metadata but does not by itself prove the content is correct. Open images and PDFs with the intended authorized download mechanism.

The provider exposes buffer and stream routes for other consumers. A Nest controller's serialized buffer response is not automatically the same as a browser-ready binary download. Use the response contract actually returned by that route; the advanced verification notes below distinguish tested responses from merely exposed methods.

Upload supplier-sheet.pdf through the same multipart route, using location=course-files-1009/private and text isPrivate=true. Keep its path. The controlled test showed its unsigned URL returned HTTP 403, while an authorized signed URL returned the PDF with HTTP 200.

For a fresh link call POST /repository/file/signurl:

{
  "location": "YOUR_ORG/course-files-1009/private/supplier-sheet.pdf",
  "options": { "expiry": 900 }
}

The response is an object containing signedUrl and raw. expiry is seconds; 900 requests fifteen minutes. Always set an explicit duration. The provider's current fallback when options are omitted is approximately ten years, so omission does not mean a short default. Private links generated by the listing helper follow a separate fifteen-minute setting.

A signed URL is usable by its holder without your staff bearer token. Authorize the download in your application before issuing it; keep staff credentials out of customer browsers. These access checks prove the behavior of the tested objects and provider configuration, not universal cross-customer isolation.

Actual File Manager private document folder

Actual document folder. The independent unsigned403/signed200 requests establish access; a folder name or badge alone would not.

10. Change public/private visibility

Call POST /repository/file/make-public or POST /repository/file/make-private with locations, plural:

{
  "locations": ["YOUR_ORG/course-files-1009/private/supplier-sheet.pdf"]
}

Inspect the returned per-object outcomes. A successful HTTP response can contain individual errors. The lab made its harmless PDF public and confirmed an unsigned HTTP 200, then private and confirmed unsigned HTTP 403. Recheck access in a fresh request instead of relying on an old browser cache or a privacy badge alone.

Public/private visibility and signed-link expiration are separate. Changing an object to private does not necessarily invalidate a previously issued signed link. Choose short expiry and verify the storage provider's behavior if immediate revocation is a requirement. Generated image previews also need their access checked when an image's visibility changes.

11. Use customer file operations

Use Authentication and Organization Isolation: credential choices and its complete customer-session lab to obtain a real customer session. The direct route carries the customer token in Authorization; an app-mediated route also has an app token and puts the customer token in x-client-authorization. Do not claim both tokens are mandatory for every supported client.

The file controller currently resolves CurrentCustomerOrUser, so an eligible staff identity is a different actor from a customer. The customer upload body is JSON:

{
  "fileName": "course-notes.txt",
  "base64Data": "data:text/plain;base64,QXBwRW5naW5lIGZpbGUgdGVzdAo="
}

Call POST /repository/customer/file/upload, then POST /repository/customer/file/flatlist with {"prefix":"course-notes.txt"}. The source places files under the acting identity's users/<sk>/ folder. Customer delete uses POST /repository/customer/file/delete and {"filePath":"course-notes.txt"}.

Current verified limitation: a valid direct customer token was refused with HTTP 403 by both upload and list in this deployment. The app-token plus customer-header pattern is a configuration reference, not yet a successful demonstrated outcome. The required app credential was not available for this verification run. Do not substitute a broad staff token into a customer frontend to bypass this result. Successful upload/list/delete and the actual caller/header combination must be checked together. A correctly prefixed filename alone does not establish every cross-customer boundary.

12. Copy, move and replace files

Copy and move use two paths:

{
  "src": "YOUR_ORG/course-files-1009/private/supplier-notes.txt",
  "dest": "course-files-1009/working/supplier-notes-copy.txt"
}

POST /repository/file/copy leaves the source; POST /repository/file/move relocates it. Inspect both paths with exists afterward. Copy checks the storage allowance. The implementation attempts to preserve access, but checking the destination URL is still necessary: a logged ACL failure is not the same as guaranteed preserved public access. For images, inspect regenerated or leftover previews too.

To replace an attachment without deliberately overwriting a shared path:

  1. Upload the replacement under a distinct filename.
  2. Inspect its bytes or preview and keep the new returned path.
  3. Read the owning record and update its reference, preserving other attachment entries.
  4. Read that same record back and open the new image from the intended interface.
  5. Delete the old file only if no other record still uses it.

This is a sequence of separate operations, not a transaction across storage and Repository. Keep the old object until the record change succeeds. Uploading to the same filename can overwrite the object used by every reference to that path; browser and CDN caches may retain the earlier content. Distinct filenames make replacement and recovery easier to follow.

13. Import from a URL

POST /repository/file/upload-url downloads a source and stores a new object:

{
  "url": "AN_EXISTING_IMAGE_URL_YOU_CONTROL",
  "location": "course-files-1009/imported",
  "name": "cedar-import.png",
  "thumbnails": true,
  "isPrivate": false
}

This is not merely saving an external link. The server fetches the bytes, detects the file type, builds the destination filename and runs the storage path. It must be able to reach the URL. Use an owned supported image in this exercise; do not assume arbitrary text or every remote document works with the type-detection path. Read the returned path and verify the imported object before attaching it.

The isPrivate value here is a JSON boolean, unlike the multipart text field. The response has the same object/preview shape as the underlying upload. Pass the intended visibility explicitly rather than accidentally importing a document as public.

14. Folders, previews and indexed file records

Create a folder marker with POST /repository/file/createfolder and {"location":"course-files-1009/working/"}. The trailing slash is deliberate. Object-storage folders are prefixes/markers; do not treat them as a filesystem permission boundary.

Regenerate previews with POST /repository/file/thumbnails and {"location":"THE_SAVED_IMAGE_PATH"}. One path or an array is supported. The operation returns an array describing the results; inspect every preview you intend to use.

Index a folder with POST /repository/file/index:

{
  "location": "course-files-1009/",
  "skipExisting": true
}

Then query POST /repository/find/fileinfo for the exact data.path. The source creates or updates file-reference records with path, url and content:null. This is not PDF full-text extraction. skipExisting:true preserves existing index entries; omit or change it only when you intend to refresh them. The indexing response is a storage listing, so verify the resulting records separately. Deleted objects and stale indexed references can diverge; the index is not proof of current object existence.

15. Advanced file operations and limits

OperationExact body or methodPurpose and boundary
Append/prependPOST file/append or file/prepend; {location,content}Both returned HTTP 500 in the tested S3 deployment; readback confirmed unchanged text. Upload a revised file instead.
DriverGET file/driverReturns safe driver metadata such as { "driver":"s3" }; not storage credentials.
BufferPOST file/buffer; {location}Verified HTTP 201 with content: {type:"Buffer",data:[bytes]} and raw; JSON byte data, not a direct file download.
StreamPOST file/stream; {location}Returned a serialized internal stream object with HTTP 201, not usable file bytes. Use the tested authorized URL flow for downloads.
FaviconPOST file/create_favicon; {location}Writes organization-root favicon.ico and favicon.png; can replace existing icons.
Shared assetPOST file/get_asset; {location,encoding}Reads the configured shared-organization asset space; not the current organization's ordinary file read.

All paths above are relative to /repository/. Internally implemented multipart start/upload-part/complete/abort helpers currently support bulk export; they are not a documented public resumable-upload route. Do not construct nonexistent controller endpoints from provider method names.

Upload, URL import and copy check the organization's storage allowance and can reject with HTTP 403 and reason storage_limit. A full plan requires freeing space or adjusting the plan, not repeatedly sending the same file. Do not claim that every write route performs an identical quota check. File size/request limits and supported storage behavior also depend on the deployment; inspect its current response rather than inventing a universal maximum.

16. Delete and verify cleanup

POST /repository/file/delete uses location, singular; its value may be one path or an array:

{
  "location": ["YOUR_ORG/course-files-1009/working/supplier-notes-copy.txt"]
}

The result is an array of outcomes with wasDeleted. The executed deletion returned wasDeleted:null; a subsequent exists request returned false. Inspect each entry and then use exists or a new listing rather than treating null as proof of either success or failure. A delete request can report a per-file failure without changing the HTTP status. Generated image previews are included in the provider's deletion attempt, but inspect the final result rather than assuming all derivatives disappeared.

Retain the image still used by the course product. Remove only disposable copy/move/import fixtures that are no longer referenced. Removing a file does not rewrite a product's images or necessarily remove its fileinfo index record. If a record still holds a broken reference, update that record separately and read it back. If you delete an owning record, decide independently whether its files remain needed elsewhere.

17. Diagnose the failed layer

SymptomCheckCorrection
Upload rejects with missing fileMultipart field name/type and client boundarySend binary in file; let the client set Content-Type.
Upload is stored but product has no imageExact product ID and data.images readbackSave the file reference on the intended record.
URL exists but opening failsObject existence, public access, signed expiryInspect exists/access and obtain a fresh authorized link.
Original works but preview failsActual preview keys and opening resultRegenerate supported previews; keep original upload and preview outcomes separate.
New image appears staleSame-path overwrite, browser/CDN cache, record referenceUse a distinct replacement name and confirm the returned path.
Listing and file search disagreeStorage prefix versus indexed fileinfo recordsInspect the object and index separately; refresh only the needed index.
HTTP success contains failuresPer-file result/error fieldsResolve the specific path or provider issue before repeating the batch.
Storage limit reachedResponse reason storage_limitFree space or adjust the plan; preserve still-used files.

Limitations and checks

The current source review covers repository.controller.ts, repository.provider.file.ts, repository.provider.file.s3.ts and repository.assets.service.ts in AppEngine. Sanitized executed proofs are stored under videos/appengine-files-storage/production/lab/; no bearer credentials are part of the learner examples.

Verified: staff session; public PNG upload and anonymous read; private text/PDF uploads; exists/stat/text and buffer reads; explicit 900-second signed link plus a separate short link rejected after expiry; product gallery attachment and readback; public/private PDF changes checked through unsigned requests; copy/move and both path checks; distinct replacement upload; URL import; folder creation; thumbnail regeneration; file indexing and query; driver metadata; favicon generation and cleanup; deletion followed by exists:false. Append/prepend returned 500 without changing the file. Stream returned an internal object. Direct customer upload/list returned 403; the app-and-customer flow remains pending. Actual Studio navigation and product-gallery readback are included in the 20-chapter, 1440p video review cut. Final encoded-picture, narration-boundary and browser-playback checks passed. This export does not turn the unverified customer flow into a demonstrated result.