{"id":13605950,"url":"https://github.com/K1rakishou/Fuck-Storage-Access-Framework","last_synced_at":"2025-04-12T05:35:21.259Z","repository":{"id":38361409,"uuid":"207005461","full_name":"K1rakishou/Fuck-Storage-Access-Framework","owner":"K1rakishou","description":"Fuck Storage Access Framework (or just FSAF) is a handy library that hides away from you all the annoying parts of the Storage Access Framework (like DocumentTrees / DocumentIds / DocumentFiles / DocumentContracts and other bullshit) leaving only an API that is similar to good-old Java File API ","archived":false,"fork":false,"pushed_at":"2022-08-15T09:32:05.000Z","size":423,"stargazers_count":281,"open_issues_count":3,"forks_count":14,"subscribers_count":5,"default_branch":"develop","last_synced_at":"2024-11-07T10:43:22.247Z","etag":null,"topics":["android","kotlin","saf","storage-access-framework"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/K1rakishou.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-09-07T17:51:25.000Z","updated_at":"2024-08-21T21:24:30.000Z","dependencies_parsed_at":"2022-09-21T05:05:56.287Z","dependency_job_id":null,"html_url":"https://github.com/K1rakishou/Fuck-Storage-Access-Framework","commit_stats":null,"previous_names":[],"tags_count":50,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/K1rakishou%2FFuck-Storage-Access-Framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/K1rakishou%2FFuck-Storage-Access-Framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/K1rakishou%2FFuck-Storage-Access-Framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/K1rakishou%2FFuck-Storage-Access-Framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/K1rakishou","download_url":"https://codeload.github.com/K1rakishou/Fuck-Storage-Access-Framework/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248524656,"owners_count":21118612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","kotlin","saf","storage-access-framework"],"created_at":"2024-08-01T19:01:04.599Z","updated_at":"2025-04-12T05:35:20.943Z","avatar_url":"https://github.com/K1rakishou.png","language":"Kotlin","funding_links":[],"categories":["Kotlin"],"sub_categories":[],"readme":"# Fuck Storage Access Framework (FSAF)\n\n\u003cp align=\"left\"\u003e\u003ca href=\"https://jitpack.io/#K1rakishou/Fuck-Storage-Access-Framework/v1.0\"\u003e\u003cimg src=\"https://jitpack.io/v/K1rakishou/Fuck-Storage-Access-Framework.svg\" width=\"120\"\u003e\u003c/a\u003e\u003c/p\u003e \n\nIf you ever had to deal with Storage Access Framework you must understand the pain \nimposed on you by it's API and lack of any good examples.\n\nThis tiny library attempts to hide away that API, providing a well-known Java File-like API instead, \nabstracting away both the SAF files (or DocumentFiles) and the standard Java Files. \nBasically with this library you don't even need to think about what kind of file to use because \nit's all being figured out internally.\n\nThe SAF is slow, especially when you want to do some file operations with many different files. \nThe other goal of this library is to provide an API that will significantly improve the file \noperations speed when dealing with lots of files or with nested directories with files, etc.\n\nWhen moving away from the Java File API to SAF, you may encounter problems with migration because \nsome users may not want to change their directories with files right away \n(And it will probably be a pain in the ass to do that manually). \nYet another goal of this library is to make this migration seamless. Or even leave both variants.\n\nSamples\n---\n\nLatest version:\n\n```\nimplementation 'com.github.K1rakishou:Fuck-Storage-Access-Framework:v1.1.3'\n```\n\nThere are three main scenarios when dealing with files:\n* Read or write to a user-provided file.\n* Create a new file in a user-provided directory with a user-provided name.\n* Use a user-provided directory as a file-dump throughout the app's lifetime.\n\nThe first two are usually pretty simple to implement even with the normal SAF api since it is usually\na one-time operation. But the third one is not that trivial.\nLet's see how this library helps you dealing with these three scenarios when using SAF.\n\n### Read or write to a user-provided file\n\nIt's pretty simple, just use the `FileChooser.openChooseFileDialog()`\nmethod which will return to you an Uri of the selected file:\n\n```kotlin\nfileChooser.openChooseFileDialog(object : FileChooserCallback() {\n    override fun onResult(uri: Uri) {\n      val externalFile = fileManager.fromUri(uri)\n      if (externalFile == null) {\n        println(\"Couldn't convert Uri to an ExternalFile\")\n        return\n      }\n      \n      println(\"name = ${fileManager.getName(externalFile)}\")\n    }\n\n    override fun onCancel(reason: String) {\n      println(\"Canceled by user\")\n    }\n  })\n```\n\nIn case of user selecting nothing and pressing back the `onCancel()` method will be called.\n\n### Create a new file in a user-provided directory with the user-provided name.\n\nThe same goes for the scenario where you want to create a file inside a directory chosen by the user,\nuse the `FileChooser.openCreateFileDialog()` method:\n\n```kotlin\nfileChooser.openCreateFileDialog(\"text.txt\", object : FileCreateCallback() {\n    override fun onResult(uri: Uri) {\n      val externalFile = fileManager.fromUri(uri)\n      if (externalFile == null) {\n        println(\"Couldn't convert Uri to an ExternalFile\")\n        return\n      }\n\n      println(\"exists = ${fileManager.exists(externalFile)}\")\n    }\n\n    override fun onCancel(reason: String) {\n      println(\"Canceled by user\")\n    }\n  })\n```\n\nBe aware that when creating a file with a name of already existing file in the directory, SAF will append \"(1)\" at the end of the new file.\n\n### Use a user-provided directory as a file-dump throughout the app's lifetime.\n\nThis is where things start to get interesting. First of all, you need a directory which you will then\nuse to store some files (downloaded images/videos etc). That directory will have to have the proper\nread/write permissions as well as the persistence permission. Without the persistence permission you won't\nbe able to access the directory after the phone reboot. FSAF automatically adds all the necessary\nflags to require both read/write and the persistence permission when choosing a directory (or a file) via the\nFileChooser.\n\n```kotlin\nfileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() {\n    override fun onResult(uri: Uri) {\n      println(\"treeUri = ${uri}\")\n    }\n    \n    override fun onCancel(reason: String) {\n      println(\"Canceled by user\")\n    }\n  })\n```\n\nAfter retrieving the directory's Uri you will probably need to store it somewhere so you won't lose it.\nThen you need to register a `BaseDirectory`. A `BaseDirectory` is a root directory inside of which \nyou will be able to create new files/directories/sub-directories etc. You can register your own implementation of a `BaseDirectory`\nby inheriting from a `BaseDirectory` class and by overriding the required methods:\n\n```kotlin\nclass TestBaseDirectory(\n  private val getBaseDirUriFunc: () -\u003e Uri?,\n  private val getBaseDirFileFunc: () -\u003e File?\n) : BaseDirectory() {\n\n  override fun getDirUri(): Uri? = getBaseDirUriFunc.invoke()\n  override fun getDirFile(): File? = getBaseDirFileFunc.invoke()\n  override fun currentActiveBaseDirType(): ActiveBaseDirType = ActiveBaseDirType.SafBaseDir\n}\n```\n\nThen you need to instantiate it and register it in the `FileManager`:\n\n```kotlin\nprivate val testBaseDirectory = TestBaseDirectory({\n    getTreeUri()\n  }, {\n    null\n  })\n  \nfileManager.registerBaseDir\u003cTestBaseDirectory\u003e(testBaseDirectory)\n```\n\nAnd that's it. Now you can create any file or directory inside the base directory.\n\n`BaseDirectory` requires you to override three methods:\n* `getDirUri()` you have to return the `Uri` to the base directory that was returned to you in the\n`onResult` callback after calling `FileChooser.openChooseDirectoryDialog()`. This is your base \ndirectory's Uri somewhere inside SAF. It may be on a SD-card or somewhere on the external phone memory. It\nshould always return a non-null value.\n* `getDirFile()` this is an alternative Java file backed directory. The point of it is that it's\nusually impossible to force users to switch from one thing to another immediately. So this process may take\nsome time and to make it seamless for the users you may add an ability to select\neither a Java file backed directory or a SAF backed directory. And to figure out what kind of base\ndirectory is currently being used, you need to change return value of the third overridden method.\n* `currentActiveBaseDirType()` you have to return either `ActiveBaseDirType.SafBaseDir` or\n`ActiveBaseDirType.JavaFileBaseDir` depending on what the user has selected. This method is called \nevery time you want to create a new file or directory and it's needed to figure out where exactly it should be created.\nWhen not using the alternative Java file directory you may want to always return `ActiveBaseDirType.SafBaseDir` here.\nBut you should not use it for only the Java File backed directories. Java File API is pretty fast and simple as it is.\n\nHere is how a base directory may look like when using both methods:\n\n```kotlin\nclass SavedFilesBaseDirectory(\n) : BaseDirectory() {\n\n    override fun getDirFile(): File? {\n        return File(ChanSettings.saveLocation.fileApiBaseDir.get())\n    }\n\n    override fun getDirUri(): Uri? {\n        return Uri.parse(ChanSettings.saveLocation.safBaseDir.get())\n    }\n\n    override fun currentActiveBaseDirType(): ActiveBaseDirType {\n        return when {\n            ChanSettings.saveLocation.isSafDirActive() -\u003e ActiveBaseDirType.SafBaseDir\n            ChanSettings.saveLocation.isFileDirActive() -\u003e ActiveBaseDirType.JavaFileBaseDir\n            else -\u003e throw IllegalStateException(\"SavedFilesBaseDirectory: No active base directory!!!\")\n        }\n    }\n} \n```\n\nWhere `ChanSettings.saveLocation.fileApiBaseDir` and `ChanSettings.saveLocation.safBaseDir` are just wrappers\nover shared prefs.\n\nNow that everything is set up, lets see how can we create files/directories and use a couple of\nstandard file operations.\n\n### Creating a new file or directory\n\nIt's pretty simple (especially when using Kotlin):\n\n```kotlin\nval baseDirectory: AbstractFile = fileManager.newBaseDirectoryFile\u003cTestBaseDirectory\u003e()\n```\n\nThis will instantiate a new `AbstractFile` class, but IT WILL NOT create anything on the disk yet. \nThink of it like of the regular Java File where, to physically create a file on the disk, you need to\ncall the `createNew()`/`mkdir()` method first. `AbstractFile` is a class with no `segments` and the\nroot that is pointing to the base directory. `AbstractFile` is just an abstraction over both a SAF backed\nfile/directory or a Java File backed file/directory. A `segment` may be either a directory segment (in this case it's a directory\nname) or a file segment (in this case it's a file name). Directory segments SHOULD NOT contain extensions (i.e. \".txt\").\nFile segments may or may not contain file segments. It's pretty simple. If you want to create a directory use\n`DirectorySegment` if a file use `FileSegment`. But there is one rule: after creating a `FileSegment`\nyou can't create anything anymore with that path or an exception will be thrown. Just like when using Java File API.\n\nNow lets create a couple of directories and files:\n\n```kotlin\nval file1: AbstractFile? = fileManager.create(baseDir, FileSegment(\"file1.txt\"))\nval dir1: AbstractFile? = fileManager.create(baseDir, DirectorySegment(\"dir1\"))\nval file2: AbstractFile? = fileManager.create(baseDir, DirectorySegment(\"dir1\"), FileSegment(\"file2.txt\"))\nval file3: AbstractFile? = fileManager.create(baseDir, DirectorySegment(\"dir1\"), DirectorySegment(\"dir2\"), FileSegment(\"file3.txt\"))\n```\n\nThis will create `file1.txt` and `dir1` inside the base directory. Then it will create `file2.txt`\ninside `dir1` and after that `file3.txt` inside `dir2` inside `dir1`, so it will look like this:\n\n```kotlin\nROOT/file1.txt\nROOT/dir1\nROOT/dir1/file1.txt\nROOT/dir1/file2.txt\nROOT/dir1/dir2/file3.txt\n```\n\nAnd that's it. There couple other overloaded versions of the `create()` (and even `createUnsafe()` if you\nknow what you are doing) method you can find all of them in the `FileManager` class.\n\n### Checking whether a base directory exists\n\nUser may delete your base directory at any time! So you have to check whether it exists before doing \nanything. Usually you want to do it before calling `FileManager.newBaseDirectoryFile()`. To check\nwhether a base directory exists use `FileMananger.baseDirectoryExists()` method.\n\n### Forgetting and unregistering a base directory\n\nUser may want to change a base directory at any time and you need to handle that. Before registering\na new base directory, if an old base directory still exists, you may want to give back all of the\ndirectory permissions (Well, actually nobody is forcing you not to do that but it's a good practice\nto do that). Use `FileChooser.forgetSAFTree()` method to revoke any permissions you have for that \ndirectory.\nAfter doing that you won't be able to access that directory anymore. So you might want to\nask the user whether they want to copy the files from the old base directory to a new one. Fortunately,\n`FileManager` has API to do that (`FileManager.copyDirectoryWithContent()`).\nYou may even add an ability to delete files in the old base directory after copying them into a new one.\nAnd there is also API to do that (`FileManager.deleteContent()`). You should probably NOT DELETE the base\ndirectory itself because it is a user-selected directory.\n\nAfter copying files and deleting old files you can also remove the base directory from the\n`FileManager` by using `FileManager.unregisterBaseDir()`.\n\n### Reading from/Writing to a file\n\nYou can use `FileManager.withFileDescriptor()` method for that.\nIt takes an `AbstractFile` (which must be a file not a directory!) a `FileDescriptorMode` which \ndescribes what you want to do with a file (read/write/write truncate (because by default SAF won't truncate\nold file content)) and a lambda into which the `FileDescriptor` will be passed.\n\nAlternatively you can use `FileManager.getInputStream()` or `FileManager.getOutputStream()`.\n\n### Directory snapshot\n\nSAF is slow. Every SAF file IO operation takes like 20-30ms because it uses an IPC call.\nAnd sometimes you may want to check whether a lot of files exist on the disk and if they\ndo not then create them (or something similar that requires a lot of file operations). It's so slow\nthat even in google example they use \n[hacks](https://github.com/android/storage-samples/blob/master/ActionOpenDocumentTree/app/src/main/java/com/example/android/ktfiles/CachingDocumentFile.kt#L25) \nto make it faster. Well, this library uses hacks as well to make it even more faster.\nBasically, if you need to make lots of file operations the fastest way to do this is to read the\nwhole directory (with files/sub-directories and all the file metadata like file names/file sizes etc)\nat one go (in a huge batch) into a InMemory-Tree structure and do all the necessary operations with that tree.\nThis is what snapshots are for.\n\nTo create a directory snapshot use `FileManager.createSnapshot()` method. If you want to include \nsub-directories into the snapshot as well use `includeSubDirs` parameter. After creating a snapshot\nyou can do anything you want with it. ~~but after you are done with it DO NOT FORGET TO RELEASE IT by using\n`FileManager.releaseSnapshot()`. You need provide the same `AbstractFile` as the parameter which MUST BE a directory.\nAlternatively you may use `FileManager.withSnapshot()` which will release the snapshot automatically \nfor you.~~ \n\n^\n+-- Not true anymore. You don't have to release the snapshot anymore since every snapshot is a separate self-contained object now so it can just be safely GCed.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FK1rakishou%2FFuck-Storage-Access-Framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FK1rakishou%2FFuck-Storage-Access-Framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FK1rakishou%2FFuck-Storage-Access-Framework/lists"}