Video chapters
- AppEngine File Storage APIs
- Stored files and record references
- Prepare the organization and staff session
- Upload a file with multipart form data
- Understand paths, links and image previews
- Attach the uploaded image to a product
- Inspect the file and product in Appmint Studio
- List files, folders and pages
- Check existence, metadata and file content
- Upload a private document
- Create an expiring download link
- Change access and check the actual URL
- Upload, list and delete as a customer
- Copy and move files
- Replace an attachment without breaking its reference
- Import an existing image from a URL
- Folders, thumbnails and the file index
- Remove files and check remaining references
- Additional operations, permissions and limits
- 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.
1. Understand objects, links and record references
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:
| Operation | Current permission |
|---|---|
| Upload, URL import, folder creation, thumbnail generation | Create |
| List, exists, stat, read, signed URL, file index, driver metadata | Read |
| Copy, move, append/prepend, visibility change | Update |
| Delete | Delete |
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 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 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:
| Route | Additional body | Result to inspect |
|---|---|---|
POST /repository/file/exists | None | exists boolean; storage metadata in raw |
POST /repository/file/stat | None | size, modified, raw |
POST /repository/file | "encoding":"utf8" for text | content 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.
9. Upload a private document and issue an expiring link
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 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:
- Upload the replacement under a distinct filename.
- Inspect its bytes or preview and keep the new returned path.
- Read the owning record and update its reference, preserving other attachment entries.
- Read that same record back and open the new image from the intended interface.
- 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
| Operation | Exact body or method | Purpose and boundary |
|---|---|---|
| Append/prepend | POST 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. |
| Driver | GET file/driver | Returns safe driver metadata such as { "driver":"s3" }; not storage credentials. |
| Buffer | POST file/buffer; {location} | Verified HTTP 201 with content: {type:"Buffer",data:[bytes]} and raw; JSON byte data, not a direct file download. |
| Stream | POST file/stream; {location} | Returned a serialized internal stream object with HTTP 201, not usable file bytes. Use the tested authorized URL flow for downloads. |
| Favicon | POST file/create_favicon; {location} | Writes organization-root favicon.ico and favicon.png; can replace existing icons. |
| Shared asset | POST 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
| Symptom | Check | Correction |
|---|---|---|
| Upload rejects with missing file | Multipart field name/type and client boundary | Send binary in file; let the client set Content-Type. |
| Upload is stored but product has no image | Exact product ID and data.images readback | Save the file reference on the intended record. |
| URL exists but opening fails | Object existence, public access, signed expiry | Inspect exists/access and obtain a fresh authorized link. |
| Original works but preview fails | Actual preview keys and opening result | Regenerate supported previews; keep original upload and preview outcomes separate. |
| New image appears stale | Same-path overwrite, browser/CDN cache, record reference | Use a distinct replacement name and confirm the returned path. |
| Listing and file search disagree | Storage prefix versus indexed fileinfo records | Inspect the object and index separately; refresh only the needed index. |
| HTTP success contains failures | Per-file result/error fields | Resolve the specific path or provider issue before repeating the batch. |
| Storage limit reached | Response reason storage_limit | Free 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.