package expo.modules.filesystem import android.net.Uri import android.util.Base64 import expo.modules.interfaces.filesystem.Permission import expo.modules.kotlin.apifeatures.EitherType import expo.modules.kotlin.typedarray.TypedArray import java.io.FileOutputStream import java.security.MessageDigest @OptIn(EitherType::class) class FileSystemFile(uri: Uri) : FileSystemPath(uri) { // Kept empty for now, but can be used to validate if the uri is a valid file uri. // TODO: Move to the constructor once also moved on iOS fun validatePath() { } // This makes sure that if a file already exists at a location, it is the correct type so that all available operations perform as expected. // After calling this function, we can use the `isDirectory` and `isFile` functions safely as they will match the shared class used. override fun validateType() { validatePermission(Permission.READ) if (file.exists() && file.isDirectory()) { throw InvalidTypeFileException() } } val exists: Boolean get() { return if (checkPermission(Permission.READ)) { file.isFile() } else { false } } fun create(options: CreateOptions = CreateOptions()) { validateType() validatePermission(Permission.WRITE) validateCanCreate(options) if (uri.isContentUri) { throw UnableToCreateException("create function does not work with SAF Uris, use `createDirectory` and `createFile` instead") } if (options.overwrite && exists) { javaFile.delete() } if (options.intermediates) { javaFile.parentFile?.mkdirs() } val created = javaFile.createNewFile() if (!created) { throw UnableToCreateException("file already exists or could not be created") } } fun write(content: String) { validateType() validatePermission(Permission.WRITE) if (!exists) { create() } file.outputStream().use { outputStream -> outputStream.write(content.toByteArray()) } } fun write(content: TypedArray) { validateType() validatePermission(Permission.WRITE) if (!exists) { create() } if (uri.isContentUri) { file.outputStream().use { outputStream -> val array = ByteArray(content.length) content.toDirectBuffer().get(array) outputStream.write(array) } } else { FileOutputStream(javaFile).use { it.channel.write(content.toDirectBuffer()) } } } fun write(content: ByteArray) { validateType() validatePermission(Permission.WRITE) if (!exists) { create() } if (uri.isContentUri) { file.outputStream().use { outputStream -> outputStream.write(content) } } else { FileOutputStream(javaFile).use { it.write(content) } } } fun asString(): String { val uriString = file.uri.toString() return if (uriString.endsWith("/")) uriString.dropLast(1) else uriString } fun text(): String { validateType() validatePermission(Permission.READ) return file.inputStream().use { inputStream -> inputStream.bufferedReader().use { it.readText() } } } fun base64(): String { validateType() validatePermission(Permission.READ) file.inputStream().use { return Base64.encodeToString(it.readBytes(), Base64.NO_WRAP) } } fun bytes(): ByteArray { validateType() validatePermission(Permission.READ) file.inputStream().use { return it.readBytes() } } fun asContentUri(): Uri { validateType() validatePermission(Permission.READ) return file.getContentUri(appContext ?: throw MissingAppContextException()) } @OptIn(ExperimentalStdlibApi::class) val md5: String get() { validatePermission(Permission.READ) val md = MessageDigest.getInstance("MD5") file.inputStream().use { val digest = md.digest(it.readBytes()) return digest.toHexString() } } val size: Long? get() { return if (file.exists()) { file.length() } else { null } } val type: String? get() { return file.type } fun info(options: InfoOptions?): FileInfo { validateType() validatePermission(Permission.READ) if (!file.exists()) { val fileInfo = FileInfo( exists = false, uri = slashifyFilePath(file.uri.toString()) ) return fileInfo } val fileInfo = FileInfo( exists = true, uri = slashifyFilePath(file.uri.toString()), size = size, modificationTime = modificationTime, creationTime = creationTime ) if (options != null && options.md5 == true) { fileInfo.md5 = md5 } return fileInfo } }