A Capacitor plugin for secure, user-approved file and folder access on iOS and Android, built on top of platform-native Security Scoped Bookmarks (iOS) and Storage Access Framework (SAF) (Android).
@daniele-rolli/capacitor-scoped-storage
This plugin lets your app request a folder from the user and then perform safe file operations (read, write, append, delete, move, copy, stat, etc.) within that scope, with persistent access across app restarts.
π For a real-world setup, check out the example-app with a full implementation.
-
π Scoped access: Operates only within the folder the user selects
-
π Cross-platform:
- iOS β Security-scoped bookmarks
- Android β Storage Access Framework (SAF)
-
π File operations:
readFile,writeFile,appendFile,deleteFile -
π¦ Directory management:
mkdir,rmdir,readdir -
π Metadata utilities:
stat,exists -
π Path management:
move,copy,getUriForPath -
Persistent folder permissions (reusable bookmarks/tree URIs)
-
UTF-8 and Base64 encoding support
npm install @daniele-rolli/capacitor-scoped-storage
npx cap syncRequirements:
- iOS deployment target: 14.0+
- Android: API 21+
const { folder } = await ScopedStorage.pickFolder();
// { folder: { id: "...", name: "Documents" } }await ScopedStorage.writeFile({
folder,
path: 'notes/today.txt',
data: 'Hello world!',
});
const res = await ScopedStorage.readFile({
folder,
path: 'notes/today.txt',
});
console.log(res.data); // "Hello world!"const { entries } = await ScopedStorage.readdir({ folder, path: 'notes' });
console.log(entries);
// [{ name: "today.txt", isDir: false, size: 123, mtime: 1710000000 }]await ScopedStorage.move({
folder,
from: 'notes/today.txt',
to: 'archive/today.txt',
overwrite: true,
});
await ScopedStorage.copy({
folder,
from: 'archive/today.txt',
to: 'backup/today.txt',
});pickFolder(): Promise<PickFolderResult>writeFile(options: WriteOptions): Promise<void>appendFile(options: AppendOptions): Promise<void>readFile(options: ReadOptions): Promise<{ data: string }>mkdir(options: MkdirOptions): Promise<void>rmdir(options: RmdirOptions): Promise<void>readdir(options: ReaddirOptions): Promise<ReaddirResult>stat(options: StatOptions): Promise<StatResult>exists(options: ExistsOptions): Promise<ExistsResult>deleteFile(options: DeleteOptions): Promise<void>move(options: MoveCopyOptions): Promise<void>copy(options: MoveCopyOptions): Promise<void>getUriForPath(options: UriForPathOptions): Promise<UriForPathResult>
export interface FolderRef {
/** Android: tree URI; iOS: base64 security-scoped bookmark */
id: string;
name?: string;
}
export interface PickFolderResult {
folder: FolderRef;
}export interface WriteOptions {
folder: FolderRef;
path: string; // relative to folder
data: string; // utf8 or base64
encoding?: 'utf8' | 'base64';
}
export interface AppendOptions extends WriteOptions {}
export interface ReadOptions {
folder: FolderRef;
path: string;
encoding?: 'utf8' | 'base64';
}export interface MkdirOptions {
folder: FolderRef;
path: string;
recursive?: boolean;
}
export interface RmdirOptions {
folder: FolderRef;
path: string;
recursive?: boolean;
}
export interface ReaddirOptions {
folder: FolderRef;
path?: string;
}
export interface ReaddirResult {
entries: {
name: string;
isDir: boolean;
size?: number | null;
mtime?: number | null;
}[];
}export interface StatOptions {
folder: FolderRef;
path: string;
}
export interface StatResult {
uri?: string;
size?: number | null;
mtime?: number | null;
type: 'file' | 'directory' | 'unknown';
}
export interface ExistsOptions {
folder: FolderRef;
path: string;
}
export interface ExistsResult {
exists: boolean;
isDirectory: boolean;
}export interface DeleteOptions {
folder: FolderRef;
path: string;
}
export interface MoveCopyOptions {
folder: FolderRef;
from: string;
to: string;
overwrite?: boolean;
}export interface UriForPathOptions {
folder: FolderRef;
path: string;
}
export interface UriForPathResult {
uri: string | null;
}Add the following to your Info.plist:
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UISupportsDocumentBrowser</key>
<false/>MIT Author: Daniele Rolli