/* * StorageFile.kt * Copyright (C) 2009-2021 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ package org.moire.ultrasonic.util import android.content.res.AssetFileDescriptor import android.net.Uri import com.github.k1rakishou.fsaf.FileManager import com.github.k1rakishou.fsaf.document_file.CachingDocumentFile import com.github.k1rakishou.fsaf.file.AbstractFile import com.github.k1rakishou.fsaf.file.RawFile import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.io.OutputStream import org.moire.ultrasonic.app.UApp import timber.log.Timber /** * Provides filesystem access abstraction which works * both on File based paths and Storage Access Framework Uris */ class StorageFile private constructor( private var parent: StorageFile?, private var abstractFile: AbstractFile, private var fileManager: FileManager ): Comparable { override fun compareTo(other: StorageFile): Int { return getPath().compareTo(other.getPath()) } override fun toString(): String { return name } var name: String = fileManager.getName(abstractFile) var isDirectory: Boolean = fileManager.isDirectory(abstractFile) var isFile: Boolean = fileManager.isFile(abstractFile) fun length(): Long = fileManager.getLength(abstractFile) fun lastModified(): Long = fileManager.lastModified(abstractFile) fun delete(): Boolean = fileManager.delete(abstractFile) fun listFiles(): Array { val fileList = fileManager.listFiles(abstractFile) return fileList.map { file -> StorageFile(this, file, fileManager) }.toTypedArray() } fun getFileOutputStream(): OutputStream { if (isRawFile()) return File(abstractFile.getFullPath()).outputStream() return fileManager.getOutputStream(abstractFile) ?: throw IOException("Couldn't retrieve OutputStream") } fun getFileOutputStream(append: Boolean): OutputStream { if (isRawFile()) return FileOutputStream(File(abstractFile.getFullPath()), append) val mode = if (append) "wa" else "w" val descriptor = UApp.applicationContext().contentResolver.openAssetFileDescriptor( abstractFile.getFileRoot().holder.uri(), mode) return descriptor?.createOutputStream() ?: throw IOException("Couldn't retrieve OutputStream") } fun getFileInputStream(): InputStream { if (isRawFile()) return FileInputStream(abstractFile.getFullPath()) return fileManager.getInputStream(abstractFile) ?: throw IOException("Couldn't retrieve InputStream") } // TODO there are a few functions which could be getters // They are functions for now to help us distinguish them from similar getters in File. These can be changed after the refactor is complete. fun getPath(): String { if (isRawFile()) return abstractFile.getFullPath() if (getParent() != null) return getParent()!!.getPath() + "/" + name return Uri.parse(abstractFile.getFullPath()).toString() } fun getParent(): StorageFile? { if (isRawFile()) { return StorageFile( null, fileManager.fromRawFile(File(abstractFile.getFullPath()).parentFile!!), fileManager ) } return parent } fun isRawFile(): Boolean { return abstractFile is RawFile } fun getRawFilePath(): String? { return if (abstractFile is RawFile) abstractFile.getFullPath() else null } fun getDocumentFileDescriptor(openMode: String): AssetFileDescriptor? { return if (abstractFile !is RawFile) { UApp.applicationContext().contentResolver.openAssetFileDescriptor( abstractFile.getFileRoot().holder.uri(), openMode ) } else null } companion object { // TODO it would be nice to check the access rights and reset the cache directory on error private val MusicCacheFileManager: Lazy = lazy { val manager = FileManager(UApp.applicationContext()) manager.registerBaseDir(MusicCacheBaseDirectory()) manager } fun getFromParentAndName(parent: StorageFile, name: String): StorageFile { val file = parent.fileManager.findFile(parent.abstractFile, name) ?: parent.fileManager.createFile(parent.abstractFile, name)!! return StorageFile(parent, file, parent.fileManager) } fun getMediaRoot(): StorageFile { return StorageFile( null, MusicCacheFileManager.value.newBaseDirectoryFile()!!, MusicCacheFileManager.value ) } // TODO sometimes getFromPath is called after isPathExists, but the file may be gone because it was deleted in another thread. // Create a function where these two are merged fun getFromPath(path: String): StorageFile { Timber.v("StorageFile getFromPath %s", path) val normalizedPath = normalizePath(path) if (!normalizedPath.isUri()) { return StorageFile( null, MusicCacheFileManager.value.fromPath(normalizedPath), MusicCacheFileManager.value ) } val segments = getUriSegments(normalizedPath) ?: throw IOException("Can't get path because the root has changed") var file = StorageFile(null, getMediaRoot().abstractFile, MusicCacheFileManager.value) segments.forEach { segment -> file = StorageFile( file, MusicCacheFileManager.value.findFile(file.abstractFile, segment) ?: throw IOException("File not found"), file.fileManager ) } return file } fun getOrCreateFileFromPath(path: String): StorageFile { val normalizedPath = normalizePath(path) if (!normalizedPath.isUri()) { File(normalizedPath).createNewFile() return StorageFile( null, MusicCacheFileManager.value.fromPath(normalizedPath), MusicCacheFileManager.value ) } val segments = getUriSegments(normalizedPath) ?: throw IOException("Can't get path because the root has changed") var file = StorageFile(null, getMediaRoot().abstractFile, MusicCacheFileManager.value) segments.forEach { segment -> file = StorageFile( file, MusicCacheFileManager.value.findFile(file.abstractFile, segment) ?: MusicCacheFileManager.value.createFile(file.abstractFile, segment)!!, file.fileManager ) } return file } fun isPathExists(path: String): Boolean { val normalizedPath = normalizePath(path) if (!normalizedPath.isUri()) return File(normalizedPath).exists() val segments = getUriSegments(normalizedPath) ?: return false var file = getMediaRoot().abstractFile segments.forEach { segment -> file = MusicCacheFileManager.value.findFile(file, segment) ?: return false } return true } fun createDirsOnPath(path: String) { val normalizedPath = normalizePath(path) if (!normalizedPath.isUri()) { File(normalizedPath).mkdirs() return } val segments = getUriSegments(normalizedPath) ?: throw IOException("Can't get path because the root has changed") var file = getMediaRoot().abstractFile segments.forEach { segment -> file = MusicCacheFileManager.value.createDir(file, segment) ?: throw IOException("Can't create directory") } } fun rename(pathFrom: String, pathTo: String) { val normalizedPathFrom = normalizePath(pathFrom) val normalizedPathTo = normalizePath(pathTo) Timber.d("Renaming from %s to %s", normalizedPathFrom, normalizedPathTo) val fileFrom = getFromPath(normalizedPathFrom) val parentTo = getFromPath(FileUtil.getParentPath(normalizedPathTo)!!) val fileTo = getFromParentAndName(parentTo, FileUtil.getNameFromPath(normalizedPathTo)) MusicCacheFileManager.value.copyFileContents(fileFrom.abstractFile, fileTo.abstractFile) fileFrom.delete() } private fun getUriSegments(uri: String): List? { val rootPath = getMediaRoot().getPath() if (!uri.startsWith(rootPath)) return null val pathWithoutRoot = uri.substringAfter(rootPath) return pathWithoutRoot.split('/').filter { it.isNotEmpty() } } private fun normalizePath(path: String): String { // FSAF replaces spaces in paths with "_", so we must do the same everywhere // TODO paths sometimes contain double "/". These are currently replaced to single one. // The nice solution would be to check and fix why this happens return path.replace(' ', '_').replace(Regex("(? ActiveBaseDirType.SafBaseDir else -> ActiveBaseDirType.JavaFileBaseDir } } } fun String.isUri(): Boolean { // TODO is there a better way to tell apart a path and an URI? return this.contains(':') }