@ -3,7 +3,7 @@ import {
PlatformService ,
PlatformCapabilities ,
} from "../PlatformService" ;
import { Filesystem , Directory } from "@capacitor/filesystem" ;
import { Filesystem , Directory , Encoding } from "@capacitor/filesystem" ;
import { Camera , CameraResultType , CameraSource } from "@capacitor/camera" ;
import { FilePicker } from "@capawesome/capacitor-file-picker" ;
import { logger } from "../../utils/logger" ;
@ -31,6 +31,113 @@ export class CapacitorPlatformService implements PlatformService {
} ;
}
/ * *
* Checks and requests storage permissions if needed
* @returns Promise that resolves when permissions are granted
* @throws Error if permissions are denied
* /
private async checkStoragePermissions ( ) : Promise < void > {
try {
const logData = {
platform : this.getCapabilities ( ) . isIOS ? "iOS" : "Android" ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Checking storage permissions" ,
JSON . stringify ( logData , null , 2 ) ,
) ;
if ( this . getCapabilities ( ) . isIOS ) {
// iOS uses different permission model
return ;
}
// Try to access a test directory to check permissions
try {
await Filesystem . stat ( {
path : "/storage/emulated/0/Download" ,
directory : Directory.Documents ,
} ) ;
logger . log (
"Storage permissions already granted" ,
JSON . stringify ( { timestamp : new Date ( ) . toISOString ( ) } , null , 2 ) ,
) ;
return ;
} catch ( error : unknown ) {
const err = error as Error ;
const errorLogData = {
error : {
message : err.message ,
name : err.name ,
stack : err.stack ,
} ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
// "File does not exist" is expected and not a permission error
if ( err . message === "File does not exist" ) {
logger . log (
"Directory does not exist (expected), proceeding with write" ,
JSON . stringify ( errorLogData , null , 2 ) ,
) ;
return ;
}
// Check for actual permission errors
if (
err . message . includes ( "permission" ) ||
err . message . includes ( "access" )
) {
logger . log (
"Permission check failed, requesting permissions" ,
JSON . stringify ( errorLogData , null , 2 ) ,
) ;
// The Filesystem plugin will automatically request permissions when needed
// We just need to try the operation again
try {
await Filesystem . stat ( {
path : "/storage/emulated/0/Download" ,
directory : Directory.Documents ,
} ) ;
logger . log (
"Storage permissions granted after request" ,
JSON . stringify ( { timestamp : new Date ( ) . toISOString ( ) } , null , 2 ) ,
) ;
return ;
} catch ( retryError : unknown ) {
const retryErr = retryError as Error ;
throw new Error (
` Failed to obtain storage permissions: ${ retryErr . message } ` ,
) ;
}
}
// For any other error, log it but don't treat as permission error
logger . log (
"Unexpected error during permission check" ,
JSON . stringify ( errorLogData , null , 2 ) ,
) ;
return ;
}
} catch ( error : unknown ) {
const err = error as Error ;
const errorLogData = {
error : {
message : err.message ,
name : err.name ,
stack : err.stack ,
} ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . error (
"Error checking/requesting permissions" ,
JSON . stringify ( errorLogData , null , 2 ) ,
) ;
throw new Error ( ` Failed to obtain storage permissions: ${ err . message } ` ) ;
}
}
/ * *
* Reads a file from the app ' s data directory .
* @param path - Relative path to the file in the app ' s data directory
@ -57,48 +164,175 @@ export class CapacitorPlatformService implements PlatformService {
* /
async writeFile ( path : string , content : string ) : Promise < void > {
try {
const logData = {
targetPath : path ,
contentLength : content.length ,
platform : this.getCapabilities ( ) . isIOS ? "iOS" : "Android" ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Starting writeFile operation" ,
JSON . stringify ( logData , null , 2 ) ,
) ;
// Check and request storage permissions if needed
await this . checkStoragePermissions ( ) ;
// Let user pick save location first
const result = await FilePicker . pickDirectory ( ) ;
logger . log ( "FilePicker result path:" , result . path ) ;
const pickerLogData = {
path : result.path ,
platform : this.getCapabilities ( ) . isIOS ? "iOS" : "Android" ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log ( "FilePicker result:" , JSON . stringify ( pickerLogData , null , 2 ) ) ;
// Handle paths based on platform
let cleanPath = result . path ;
if ( this . getCapabilities ( ) . isIOS ) {
// For iOS, keep content: prefix
const iosLogData = {
originalPath : cleanPath ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log ( "Processing iOS path" , JSON . stringify ( iosLogData , null , 2 ) ) ;
cleanPath = result . path ;
} else {
// For Android, extract the actual path from the content URI
const pathMatch = result . path . match ( /tree\/(.*?)(?:\/|$)/ ) ;
logger . log ( "Path match result:" , pathMatch ) ;
if ( pathMatch ) {
const decodedPath = decodeURIComponent ( pathMatch [ 1 ] ) ;
logger . log ( "Decoded path:" , decodedPath ) ;
// Convert primary:Download to /storage/emulated/0/Download
cleanPath = decodedPath . replace ( 'primary:' , '/storage/emulated/0/' ) ;
const androidLogData = {
originalPath : cleanPath ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Processing Android path" ,
JSON . stringify ( androidLogData , null , 2 ) ,
) ;
// For Android, use the content URI directly
if ( cleanPath . startsWith ( "content://" ) ) {
const uriLogData = {
uri : cleanPath ,
filename : path ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Using content URI for Android:" ,
JSON . stringify ( uriLogData , null , 2 ) ,
) ;
// Extract the document ID from the content URI
const docIdMatch = cleanPath . match ( /tree\/(.*?)(?:\/|$)/ ) ;
if ( docIdMatch ) {
const docId = docIdMatch [ 1 ] ;
const docIdLogData = {
docId ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Extracted document ID:" ,
JSON . stringify ( docIdLogData , null , 2 ) ,
) ;
// Use the document ID as the path
cleanPath = docId ;
}
}
}
// For Android, ensure we're using the correct external storage path
if ( this . getCapabilities ( ) . isMobile && ! this . getCapabilities ( ) . isIOS ) {
logger . log ( "Before Android path conversion:" , cleanPath ) ;
cleanPath = cleanPath . replace ( 'primary:' , '/storage/emulated/0/' ) ;
logger . log ( "After Android path conversion:" , cleanPath ) ;
}
const finalPath = ` ${ cleanPath } / ${ path } ` ;
logger . log ( "Final path for writeFile:" , finalPath ) ;
const finalPath = cleanPath ;
const finalPathLogData = {
fullPath : finalPath ,
filename : path ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Final path details:" ,
JSON . stringify ( finalPathLogData , null , 2 ) ,
) ;
// Write to the selected directory
await Filesystem . writeFile ( {
const writeLogData = {
path : finalPath ,
data : content ,
directory : Directory.External ,
recursive : true ,
} ) ;
contentLength : content.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Attempting file write:" ,
JSON . stringify ( writeLogData , null , 2 ) ,
) ;
} catch ( error ) {
logger . error ( "Error saving file:" , error ) ;
throw new Error ( "Failed to save file to selected location" ) ;
try {
if ( this . getCapabilities ( ) . isIOS ) {
await Filesystem . writeFile ( {
path : finalPath ,
data : content ,
directory : Directory.Documents ,
recursive : true ,
encoding : Encoding.UTF8 ,
} ) ;
} else {
// For Android, use the content URI directly
const androidPath = ` Download/ ${ path } ` ;
const directoryLogData = {
path : androidPath ,
directory : Directory.ExternalStorage ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"Android path configuration:" ,
JSON . stringify ( directoryLogData , null , 2 ) ,
) ;
await Filesystem . writeFile ( {
path : androidPath ,
data : content ,
directory : Directory.ExternalStorage ,
recursive : true ,
encoding : Encoding.UTF8 ,
} ) ;
}
const writeSuccessLogData = {
path : finalPath ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . log (
"File write successful" ,
JSON . stringify ( writeSuccessLogData , null , 2 ) ,
) ;
} catch ( writeError : unknown ) {
const error = writeError as Error ;
const writeErrorLogData = {
error : {
message : error.message ,
name : error.name ,
stack : error.stack ,
} ,
path : finalPath ,
contentLength : content.length ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . error (
"File write failed:" ,
JSON . stringify ( writeErrorLogData , null , 2 ) ,
) ;
throw new Error ( ` Failed to write file: ${ error . message } ` ) ;
}
} catch ( error : unknown ) {
const err = error as Error ;
const finalErrorLogData = {
error : {
message : err.message ,
name : err.name ,
stack : err.stack ,
} ,
timestamp : new Date ( ) . toISOString ( ) ,
} ;
logger . error (
"Error in writeFile operation:" ,
JSON . stringify ( finalErrorLogData , null , 2 ) ,
) ;
throw new Error (
` Failed to save file to selected location: ${ err . message } ` ,
) ;
}
}