
Frappe Core Files
- 25 installs
- 159 repo stars
- Updated July 8, 2026
- openaec-foundation/frappe_claude_skill_package
Helps with ai & agent building tasks.
About
frappe-core-files is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- frappe-core-files
- AI & Agent Building
- AI-coding skill
Frappe Core Files by the numbers
- 25 all-time installs (skills.sh)
- Ranked #9,764 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/frappe_claude_skill_package --skill frappe-core-filesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 159 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/frappe_claude_skill_package ↗ |
What it does
Helps with ai & agent building tasks.
Files
Frappe File Management
Quick Reference
| Action | Method | Notes |
|---|---|---|
| Save file from bytes | save_file(fname, content, dt, dn) | Returns File doc |
| Save file from URL | save_url(file_url, fname, dt, dn) | Creates File doc from URL |
| Read file content | frappe.get_file(fname) | Returns [filename, content] |
| Get file path | get_file_path(file_name) | Resolves to absolute path |
| Upload via HTTP | POST /api/method/upload_file | Multipart form upload |
| Delete file | frappe.delete_doc("File", name) | Removes doc + filesystem file |
| Attach print | frappe.attach_print(dt, dn, print_format) | Returns {"fname", "fcontent"} |
| Get cached doc | frappe.get_cached_doc("File", name) | Read-only, cached |
---
Decision Tree
What file operation do you need?
│
├─ Upload a file from user input?
│ ├─ Via web form → Attach field type (auto-handles upload)
│ └─ Via API → POST /api/method/upload_file
│
├─ Create a file programmatically?
│ ├─ From bytes/content → save_file(fname, content, dt, dn)
│ ├─ From external URL → save_url(file_url, fname, dt, dn)
│ └─ Full control → frappe.get_doc({"doctype": "File", ...}).insert()
│
├─ Read file content?
│ ├─ By filename → frappe.get_file(fname)
│ └─ By File doc → file_doc.get_content()
│
├─ Public or private?
│ ├─ Public (anyone with link) → is_private=0, URL: /files/fname
│ └─ Private (permission-based) → is_private=1, URL: /private/files/fname
│
└─ Generate PDF attachment?
└─ frappe.attach_print(doctype, name, print_format)---
File DocType: Core Fields
| Field | Type | Description |
|---|---|---|
file_name | Data | Filename without path |
file_url | Data | URL path (e.g., /files/report.pdf) |
file_type | Data | Extension (PDF, PNG, DOCX, etc.) |
is_private | Check | 0 = public, 1 = private |
is_folder | Check | True for folder entries |
folder | Link → File | Parent folder |
attached_to_doctype | Link → DocType | Parent document type |
attached_to_name | Data | Parent document name |
attached_to_field | Data | Field name on parent |
content_hash | Data | SHA-256 for deduplication |
file_size | Int | Size in bytes |
---
File URL Patterns
| Type | URL Pattern | Filesystem Path |
|---|---|---|
| Public | /files/{filename} | {site}/public/files/{filename} |
| Private | /private/files/{filename} | {site}/private/files/{filename} |
| Remote | https://... | Not stored locally |
| API | /api/method/{path} | Generated dynamically |
Valid URL prefixes: http://, https://, /api/method/, /files/, /private/files/.
ALWAYS use /private/files/ for sensitive documents. Public files are accessible to anyone with the URL, including unauthenticated users.
---
Permission Model
Frappe files use a three-tier permission model:
1. Administrator — unrestricted access to all files 2. Public files (is_private=0) — readable by anyone with the URL (no authentication required for read) 3. Private files (is_private=1) — access requires:
- User is the file owner, OR
- User has explicit share on the file, OR
- User has read permission on the
attached_to_doctype/attached_to_namedocument
NEVER store sensitive data as public files. ALWAYS set is_private=1 for documents containing personal data, financial records, or confidential information.
---
Programmatic File Operations
Save File from Content
from frappe.utils.file_manager import save_file
# Save a generated CSV
csv_content = "Name,Amount\nACME,1000\nGlobex,2000"
file_doc = save_file(
fname="report.csv",
content=csv_content.encode("utf-8"),
dt="Sales Invoice", # attach to this DocType
dn="SINV-00001", # attach to this document
folder="Home/Attachments", # optional folder
is_private=1, # private file
)
# file_doc.file_url → "/private/files/report.csv"Save File from URL
from frappe.utils.file_manager import save_url
file_doc = save_url(
file_url="https://example.com/logo.png",
filename="company-logo.png",
dt="Company",
dn="My Company",
folder="Home",
is_private=0,
)Read File Content
# By filename
filename, content = frappe.get_file("report.csv")
# By File document
file_doc = frappe.get_doc("File", {"file_name": "report.csv"})
content_bytes = file_doc.get_content()Create File Document Directly
file_doc = frappe.get_doc({
"doctype": "File",
"file_name": "generated-report.pdf",
"attached_to_doctype": "Sales Invoice",
"attached_to_name": "SINV-00001",
"is_private": 1,
"content": pdf_bytes, # raw bytes — written to disk on insert
}).insert(ignore_permissions=True)Generate and Attach PDF
# Create PDF attachment dict (for use with sendmail)
pdf_attachment = frappe.attach_print(
"Sales Invoice",
"SINV-00001",
print_format="Standard",
)
# Returns: {"fname": "Sales Invoice - SINV-00001.pdf", "fcontent": <bytes>}
# Save PDF as file attachment
from frappe.utils.file_manager import save_file
pdf = frappe.get_print("Sales Invoice", "SINV-00001", print_format="Standard", as_pdf=True)
save_file("invoice.pdf", pdf, "Sales Invoice", "SINV-00001", is_private=1)---
File Upload via REST API
# Upload file attached to a document
curl -X POST https://site.example.com/api/method/upload_file \
-H "Authorization: token api_key:api_secret" \
-F "file=@/path/to/document.pdf" \
-F "doctype=Sales Invoice" \
-F "docname=SINV-00001" \
-F "is_private=1"Response:
{
"message": {
"name": "FILE-00001",
"file_name": "document.pdf",
"file_url": "/private/files/document.pdf",
"is_private": 1
}
}---
File Size and Extension Limits
Default max file size: 10 MB per attachment.
Override in site_config.json:
{
"max_file_size": 20971520
}Max attachments per document: Set via Customize Form → Max Attachments field on the DocType.
Check file size programmatically:
from frappe.utils.file_manager import check_max_file_size
check_max_file_size(content) # raises MaxFileSizeReachedError if too large---
Attach Field Types
| Field Type | Stores | UI |
|---|---|---|
Attach | Single file URL | File picker + upload button |
Attach Image | Single image URL | Image preview + upload |
Both store the file_url string in the field value. The File DocType record is created separately with attached_to_field set.
---
S3 / Cloud Storage Integration
Frappe supports custom file storage via the delete_file_data_content hook and custom upload handlers.
S3 via frappe-s3-attachment or similar app
# In hooks.py of custom app
delete_file_data_content = "my_app.storage.delete_from_s3"ALWAYS test file deletion when using custom storage backends — the default delete_file_from_filesystem only handles local files.
Configuration Pattern
# site_config.json for S3-compatible storage
{
"s3_bucket": "my-frappe-files",
"s3_region": "eu-west-1",
"s3_access_key": "AKIA...",
"s3_secret_key": "...",
}---
Version Differences
| Feature | v14 | v15 | v16 |
|---|---|---|---|
| File DocType | Available | Available | Available |
content_hash dedup | Available | Available | Available |
| Image optimization | Manual | Auto (1920x1080, 85%) | Auto |
| Import/Export Zip | Not available | Available | Available |
---
See Also
- references/examples.md — File operation code examples
- references/anti-patterns.md — Common file handling mistakes
frappe-core-permissions— Permission model for file accessfrappe-core-database— Database operations for File queries
File Management Anti-Patterns
AP-1: Storing Sensitive Files as Public
Wrong:
save_file("payslip.pdf", content, "Salary Slip", "SAL-001", is_private=0)
# File accessible at /files/payslip.pdf — NO authentication requiredCorrect:
save_file("payslip.pdf", content, "Salary Slip", "SAL-001", is_private=1)
# File at /private/files/payslip.pdf — requires authentication + permissionNEVER store sensitive documents (payslips, contracts, ID copies, financial records) as public files. ALWAYS use is_private=1.
---
AP-2: Path Traversal in File Operations
Wrong:
# User-supplied filename — potential path traversal attack
file_path = f"/home/frappe/site/private/files/{user_input}"
with open(file_path, "rb") as f:
content = f.read()Correct:
# Use Frappe's safe file resolution
from frappe.utils.file_manager import get_file_path
file_path = get_file_path(user_input) # validates against "../" traversalALWAYS use get_file_path() or frappe.get_file() to resolve file paths. NEVER construct file paths from user input with string concatenation.
---
AP-3: Not Setting attached_to Fields
Wrong:
frappe.get_doc({
"doctype": "File",
"file_name": "report.pdf",
"content": pdf_bytes,
"is_private": 1,
# Missing attached_to_doctype and attached_to_name
}).insert()
# File is orphaned — no document link, no inherited permissionsCorrect:
frappe.get_doc({
"doctype": "File",
"file_name": "report.pdf",
"content": pdf_bytes,
"is_private": 1,
"attached_to_doctype": "Sales Invoice",
"attached_to_name": "SINV-00001",
}).insert()ALWAYS set attached_to_doctype and attached_to_name for private files. Without them, the file has no permission inheritance and becomes an orphaned record.
---
AP-4: Ignoring Max File Size
Wrong:
# Accepting arbitrary file sizes without validation
file_doc = save_file("huge-file.zip", large_content, dt, dn)
# May exhaust disk space or cause timeoutCorrect:
from frappe.utils.file_manager import check_max_file_size, save_file
check_max_file_size(large_content) # raises if exceeds limit
file_doc = save_file("data.zip", large_content, dt, dn)ALWAYS call check_max_file_size() before saving programmatically generated files that could exceed the configured limit.
---
AP-5: Deleting File Doc Without Filesystem Cleanup
Wrong:
# Direct database deletion — leaves file on disk
frappe.db.delete("File", {"name": file_name})Correct:
# Use delete_doc — triggers on_trash which cleans up filesystem
frappe.delete_doc("File", file_name, ignore_permissions=True)ALWAYS use frappe.delete_doc() to delete File records. Direct database deletion leaves orphaned files on the filesystem.
---
AP-6: Hardcoding File Paths
Wrong:
with open("/home/frappe/frappe-bench/sites/mysite/private/files/report.pdf", "rb") as f:
content = f.read()Correct:
# Use Frappe's site resolution
import os
site_path = frappe.get_site_path("private", "files", "report.pdf")
with open(site_path, "rb") as f:
content = f.read()
# Or better — use the File API
filename, content = frappe.get_file("report.pdf")NEVER hardcode bench or site paths. ALWAYS use frappe.get_site_path() or the File API.
---
AP-7: Not Handling Duplicate Files
Symptom: Multiple File records pointing to the same physical file (same content_hash).
Frappe deduplicates automatically via content_hash — multiple File docs can reference the same physical file. This is by design. However:
- NEVER delete the physical file manually — other File docs may reference it
- ALWAYS delete via
frappe.delete_doc("File", ...)— Frappe checks for other references before removing the physical file
---
AP-8: Using Attach Field Without Private Flag
Wrong: Using Attach field type for sensitive documents without setting is_private in the upload handler. By default, uploaded files may be public depending on the upload context.
Correct: Set is_private=1 in the file upload call or use a before_insert hook on File to enforce privacy for specific DocTypes:
def enforce_private_files(doc, method):
"""Force all Employee attachments to be private."""
if doc.attached_to_doctype == "Employee":
doc.is_private = 1File Management Examples
1. Upload and Attach File in Server Script
import frappe
from frappe.utils.file_manager import save_file
def attach_generated_report(doctype, docname):
"""Generate a CSV report and attach it to a document."""
rows = frappe.get_all("Sales Invoice Item",
filters={"parent": docname},
fields=["item_code", "qty", "amount"],
)
import csv
import io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=["item_code", "qty", "amount"])
writer.writeheader()
writer.writerows(rows)
file_doc = save_file(
fname=f"{docname}-items.csv",
content=output.getvalue().encode("utf-8"),
dt=doctype,
dn=docname,
is_private=1,
)
return file_doc.file_url2. Bulk File Cleanup
def remove_orphaned_files():
"""Delete private files not attached to any document."""
orphans = frappe.get_all("File",
filters={
"is_private": 1,
"attached_to_doctype": ("is", "not set"),
"attached_to_name": ("is", "not set"),
"is_folder": 0,
},
fields=["name"],
limit=100,
)
for f in orphans:
frappe.delete_doc("File", f.name, ignore_permissions=True)
frappe.db.commit()3. Copy Attachment Between Documents
def copy_attachments(source_dt, source_dn, target_dt, target_dn):
"""Copy all attachments from one document to another."""
files = frappe.get_all("File",
filters={
"attached_to_doctype": source_dt,
"attached_to_name": source_dn,
},
fields=["file_name", "file_url", "is_private"],
)
for f in files:
# Create new File doc pointing to same physical file
new_file = frappe.get_doc({
"doctype": "File",
"file_name": f.file_name,
"file_url": f.file_url,
"attached_to_doctype": target_dt,
"attached_to_name": target_dn,
"is_private": f.is_private,
}).insert(ignore_permissions=True)4. Validate File Extension Before Processing
ALLOWED_EXTENSIONS = {"pdf", "xlsx", "csv", "docx"}
def validate_attachment(doc, method):
"""Reject uploads with disallowed extensions."""
if doc.doctype != "File":
return
if doc.file_type and doc.file_type.lower() not in ALLOWED_EXTENSIONS:
frappe.throw(
f"File type '{doc.file_type}' is not allowed. "
f"Allowed: {', '.join(sorted(ALLOWED_EXTENSIONS))}"
)Hook in hooks.py:
doc_events = {
"File": {
"before_insert": "my_app.validators.validate_attachment"
}
}5. Serve Private File via Whitelisted Method
@frappe.whitelist()
def download_report(report_name):
"""Serve a private file with custom permission check."""
file_doc = frappe.get_doc("File", {"file_name": report_name, "is_private": 1})
# Custom permission check
if not frappe.has_permission("Sales Report", "read"):
frappe.throw("Not permitted", frappe.PermissionError)
filename, content = frappe.get_file(report_name)
frappe.response["filename"] = filename
frappe.response["filecontent"] = content
frappe.response["type"] = "download"6. Image Thumbnail Access
file_doc = frappe.get_doc("File", "FILE-00001")
# Thumbnail is auto-generated for images
if file_doc.thumbnail_url:
print(f"Thumbnail: {file_doc.thumbnail_url}")
# e.g., "/files/report_small.jpg"
# Force thumbnail generation
file_doc.make_thumbnail()7. Attach File to Email
# Get file content for email attachment
file_doc = frappe.get_doc("File", {"file_name": "contract.pdf"})
content = file_doc.get_content()
frappe.sendmail(
recipients=["client@example.com"],
subject="Contract",
message="<p>Please find the contract attached.</p>",
attachments=[{
"fname": file_doc.file_name,
"fcontent": content,
}],
reference_doctype="Contract",
reference_name="CONTRACT-001",
)8. Check File Size Before Save
def before_save(doc, method):
"""Enforce 5MB limit on specific DocType attachments."""
if doc.doctype != "File" or not doc.attached_to_doctype:
return
if doc.attached_to_doctype == "Employee" and doc.file_size > 5 * 1024 * 1024:
frappe.throw("Employee attachments must be under 5 MB")