├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── pull_request_template.md └── workflows │ ├── Build.yml │ └── PublishDoc.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── StorageCleaner.podspec └── StorageCleaner ├── README.md ├── Sources ├── CleanUpConstraint │ ├── CleanUpConstraint.swift │ ├── ExactStorageSpaceAvailabilityConstraint.swift │ └── PercentageStorageSpaceAvailabilityConstraint.swift ├── Cleaner.swift ├── Config │ ├── StorageCleanerConfig.swift │ └── StorageCleanerConfiguration.swift ├── DiskSpaceProvider.swift ├── Documentation.docc │ ├── GettingStarted.md │ └── StorageCleaner.md ├── Extensions │ └── URL+.swift ├── ItemRemovalResult.swift ├── LeastRecentlyModifiedComparator.swift ├── PerformanceTracker.swift ├── RemovableItems │ ├── EmptyRemovableItems.swift │ ├── RemovableFiles.swift │ └── RemovableItems.swift ├── StorageCleanerResultReceiver │ ├── AnalyticsResultReceiver.swift │ ├── DebugConsoleReciever.swift │ ├── StorageCleanerResultReceiver.swift │ └── StorageCleanerResultReceivers.swift ├── StorageCleanerWorker.swift └── StorageEnvelope │ ├── ConstrainedEnvelopeProxy.swift │ ├── DirectoryEnvelope.swift │ ├── LegacyStaleFilesEnvelope.swift │ ├── StorageEnvelope.swift │ └── StorageEnvelopes.swift └── Tests └── StorageCleanerTests.swift /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behaviour that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behaviour by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behaviour and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behaviour. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviours that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported by contacting the project team at devx@gojek.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | 48 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to StorageToolKit-iOS 2 | 3 | As the creators, and maintainers of this project, we're glad to share our projects and invite contributors to help us stay up to date. Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 6 | 7 | In general, we expect you to follow our [Code of Conduct](https://github.com/gojek/StorageToolKit-iOS/blob/main/CODE_OF_CONDUCT.md). 8 | 9 | ## Using Github Issues 10 | 11 | ### First time contributors 12 | We should encourage first time contributors. A good inspiration on this can be found [here](http://www.firsttimersonly.com/). As pointed out: 13 | 14 | > If you are an OSS project owner, then consider marking a few open issues with the label first-timers-only. The first-timers-only label explicitly announces: 15 | 16 | > "I'm willing to hold your hand so you can make your first PR. This issue is rather a bit easier than normal. And anyone who’s already contributed to open source isn’t allowed to touch this one!" 17 | 18 | By labeling issues with this `first-timers-only` label we help first time contributors step up their game and start contributing. 19 | 20 | ### Bug reports 21 | 22 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 23 | Good bug reports are extremely helpful - thank you! 24 | 25 | Guidelines for bug reports: 26 | 27 | 1. **Use the GitHub issue search** — check if the issue has already been 28 | reported. 29 | 30 | 2. **Check if the issue has been fixed** — try to reproduce it using the 31 | latest `main` or `develop` branch in the repository. 32 | 33 | 3. **Isolate the problem** — provide clear steps to reproduce. 34 | 35 | A good bug report shouldn't leave others needing to chase you up for more information. 36 | 37 | Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 38 | 39 | Example: 40 | 41 | > Short and descriptive example bug report title 42 | > 43 | > A summary of the issue and the OS environment in which it occurs. If 44 | > suitable, include the steps required to reproduce the bug. 45 | > 46 | > 1. This is the first step 47 | > 2. This is the second step 48 | > 3. Further steps, etc. 49 | > 50 | > `` - a link to the reduced test case, if possible 51 | > 52 | > Any other information you want to share that is relevant to the issue being 53 | > reported. This might include the lines of code that you have identified as 54 | > causing the bug, and potential solutions (and your opinions on their 55 | > merits). 56 | 57 | ### Feature requests 58 | 59 | Feature requests are welcome. But take a moment to find out whether your idea fits with the scope and aims of the project. It's up to *you* to make a strong case to convince the project's developers of the merits of this feature. Please provide as much detail and context as possible. 60 | 61 | Do check if the feature request already exists. If it does, give it a thumbs-up emoji or even comment. We'd like to avoid duplicate requests. 62 | 63 | ### Pull requests 64 | 65 | Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope and avoid containing unrelated commits. 66 | 67 | **Please ask first** before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project. As far as _where_ to ask, the feature request or bug report is the best place to go. 68 | 69 | Please adhere to the coding conventions used throughout a project (indentation, accurate comments, etc.) and any other requirements (such as test coverage). 70 | 71 | Follow this process if you'd like your work considered for inclusion in the project: 72 | 73 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, and configure the remotes: 74 | 75 | ```bash 76 | # Clone your fork of the repo into the current directory 77 | git clone git@github.com:YOUR_USERNAME/StorageToolKit-iOS.git 78 | # Navigate to the newly cloned directory 79 | cd StorageToolKit-iOS 80 | # Assign the original repo to a remote called "upstream" 81 | git remote add upstream git@github.com:gojek/StorageToolKit-iOS.git 82 | ``` 83 | 84 | 2. If you cloned a while ago, get the latest changes from upstream: 85 | 86 | ```bash 87 | git checkout 88 | git pull upstream 89 | ``` 90 | 91 | 3. Create a new topic branch (off the main project development branch) to 92 | contain your feature, change, or fix: 93 | 94 | ```bash 95 | git checkout -b 96 | ``` 97 | 98 | 4. Commit your changes in logical chunks. 99 | 100 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 101 | 102 | ```bash 103 | git pull [--rebase] upstream 104 | ``` 105 | 106 | 6. Push your topic branch up to your fork: 107 | 108 | ```bash 109 | git push origin 110 | ``` 111 | 112 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 113 | with a clear title and description. 114 | 115 | ### Conventions of commit messages 116 | 117 | Adding features on the repo 118 | 119 | ```bash 120 | git commit -m "feat: message about this feature" 121 | ``` 122 | 123 | Fixing features on repo 124 | 125 | ```bash 126 | git commit -m "fix: message about this update" 127 | ``` 128 | 129 | Removing features on repo 130 | 131 | ```bash 132 | git commit -m "refactor: message about this" -m "BREAKING CHANGE: message about the breaking change" 133 | ``` 134 | 135 | 136 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to 137 | license your work under the same license as that used by the project, which is available [here](https://github.com/gojek/StorageToolKit-iOS/blob/main/LICENSE.md). 138 | 139 | ### Discussions 140 | 141 | We aim to keep all project discussions inside GitHub Issues. This is to make sure valuable discussion is accessible via search. If you have questions about how to use the library, or how the project is running - GitHub Issues are the go-to tool for this project. 142 | 143 | #### Our expectations of you as a contributor 144 | 145 | We want contributors to provide ideas, keep the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an enjoyable way. 🎉 146 | 147 | The fact that you'll have push access will allow you to: 148 | 149 | - Avoid having to fork the project if you want to submit other pull requests as you'll be able to create branches directly on the project. 150 | - Help triage issues, and merge pull requests. 151 | - Pick up the project if other maintainers move their focus elsewhere. 152 | 153 | Thanks! ❤️❤️❤️❤️❤️ 154 | 155 | GO-JEK Tech -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | A clear and concise description of the changes proposed in the pull request. 4 | 5 | ## Motivation and Context 6 | 7 | Why is this change required? What problem does it solve? 8 | 9 | ## How Has This Been Tested? 10 | 11 | Please describe the tests that you ran to verify your changes. 12 | 13 | ## Screenshots (if appropriate) 14 | 15 | If applicable, add screenshots to help explain your changes. 16 | 17 | ## Types of changes 18 | 19 | What types of changes does your code introduce to the library? 20 | 21 | - [ ] Bug fix (non-breaking change which fixes an issue) 22 | - [ ] New feature (non-breaking change which adds functionality) 23 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 24 | 25 | ## Checklist 26 | 27 | - [ ] I have performed a self-review of my own code 28 | - [ ] I have commented my code, particularly in hard-to-understand areas 29 | - [ ] I have made corresponding changes to the documentation 30 | - [ ] My changes generate no new warnings 31 | - [ ] I have added tests that prove my fix is effective or that my feature works 32 | - [ ] New and existing unit tests pass locally with my changes 33 | -------------------------------------------------------------------------------- /.github/workflows/Build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: macos-12 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Build 16 | run: swift build -v 17 | - name: Run tests 18 | run: swift test -v 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/PublishDoc.yml: -------------------------------------------------------------------------------- 1 | name: PublishDocs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | docs: 25 | runs-on: macos-12 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Pages 30 | uses: actions/configure-pages@v1 31 | - name: Generate Docs Using Docc plugin 32 | run: | 33 | swift package --allow-writing-to-directory ./public \ 34 | generate-documentation --target StorageCleaner \ 35 | --disable-indexing \ 36 | --transform-for-static-hosting \ 37 | --hosting-base-path StorageToolKit-iOS \ 38 | --output-path ./public 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v1 41 | with: 42 | path: ./public 43 | 44 | deploy: 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | runs-on: ubuntu-latest 49 | needs: docs 50 | 51 | steps: 52 | - name: Deploy Docs 53 | uses: actions/deploy-pages@v1 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | Package.resolved 8 | .swiftpm/config/registries.json 9 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 10 | .netrc 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 GO-JEK Tech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "StorageCleaner", 7 | platforms: [ 8 | .iOS(.v12), 9 | .macOS(.v12), 10 | .watchOS(.v4)], 11 | products: [ 12 | .library( 13 | name: "StorageCleaner", 14 | type: .static, targets: ["StorageCleaner"]) 15 | ], 16 | dependencies: [ 17 | .package(url: "https://github.com/gojek/WorkManager.git", branch: "0.10.0"), 18 | .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0")], 19 | targets: [ 20 | .target( 21 | name: "StorageCleaner", 22 | dependencies: ["WorkManager"], 23 | path: "StorageCleaner/Sources"), 24 | .testTarget( 25 | name: "StorageCleanerTests", 26 | dependencies: ["StorageCleaner"], 27 | path: "StorageCleaner/Tests"), 28 | ] 29 | ) 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StorageToolKit 2 | 3 | StorageToolKit aims to be a set of tools that works together to identify and optimize disk usage. StorageToolKit Allows Apps to monitor and clean the stored files e.g. cache or other files that an application creates while running. 4 | 5 | 6 | ### StorageToolKit contains three libraries as of now: 7 | - StorageCleaner 8 | - StorageAnalyzerCore (TBA) 9 | - StorageAnalyzerUI (TBA) 10 | 11 | > **_NOTE:_** As of now this repo only contains StorageCleaner, we will be soon open sourcing StorageAnalyzerCore & StorageAnalyzerUI. 12 | 13 | 14 | ## Installation 15 | 16 | ### Swift Package Manager 17 | 18 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into `swift`. 19 | To add a package dependency to your Xcode project, select File > Swift Packages > Add Package Dependency and enter below repository URL 20 | 21 | ``` 22 | https://github.com/gojekfarm/StorageToolKit-iOS 23 | ``` 24 | 25 | Once you have your Swift package set up, adding StorageToolKit-iOS as a dependency is as easy as adding it to your Package.swift dependencies value. 26 | 27 | ```swift 28 | dependencies: [ 29 | .package(url: "https://github.com/gojekfarm/StorageToolKit-iOS.git", .upToNextMajor(from: "0.9.0")) 30 | ] 31 | ``` 32 | 33 | ### Cocoapods 34 | 35 | [CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate pods from StorageToolKit into your Xcode project using CocoaPods, specify the require pod in your Podfile: 36 | 37 | #### StorageCleaner 38 | 39 | ```ruby 40 | pod 'StorageCleaner' 41 | ``` 42 | 43 | ## Usage 44 | 45 | * For StorageCleaner usage refer [StorageCleaner/README.md](StorageCleaner/README.md) 46 | 47 | ## Documentation 48 | 49 | * For StorageCleaner documentation refer [StorageCleaner](https://gojek.github.io/StorageToolKit-iOS/documentation/storagecleaner) 50 | * Read the getting started article at [here](https://gojek.github.io/StorageToolKit-iOS/documentation/storagecleaner/gettingstarted) 51 | 52 | ## Contributing 53 | 54 | As the creators, and maintainers of this project, we're glad to invite contributors to help us stay up to date. Please take a moment to review [the contributing document](.github/CONTRIBUTING.md) in order to make the contribution process easy and effective for everyone involved. 55 | 56 | - If you **found a bug**, open an [issue](https://github.com/gojek/StorageToolKit-iOS/issues). 57 | - If you **have a feature request**, open an [issue](https://github.com/gojek/StorageToolKit-iOS/issues). 58 | - If you **want to contribute**, submit a [pull request](https://github.com/gojek/StorageToolKit-iOS/pulls). 59 | 60 | ## License 61 | 62 | **StorageToolKit-iOS** is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 63 | -------------------------------------------------------------------------------- /StorageCleaner.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "StorageCleaner" 3 | s.version = "0.9.0" 4 | s.summary = "StorageCleaner is a declarative disk cleaning utitlity" 5 | s.description = "StorageCleaner deletes files and other objects according to declarative configuration" 6 | 7 | s.homepage = 'https://github.com/gojek/StorageToolKit-iOS/tree/main/StorageCleaner' 8 | s.license = { :type => 'MIT', :file => 'LICENSE' } 9 | 10 | s.author = "Gojek" 11 | s.source = { :git => "https://github.com/gojek/StorageToolKit-iOS.git", :tag => "#{s.version}" } 12 | 13 | s.platform = :ios 14 | s.ios.deployment_target = '11.0' 15 | s.swift_version = '5.0' 16 | 17 | s.source_files = 'StorageCleaner/Sources/**/*.swift' 18 | s.dependency 'WorkManager', '~> 0.10.0' 19 | end 20 | -------------------------------------------------------------------------------- /StorageCleaner/README.md: -------------------------------------------------------------------------------- 1 | # StorageCleaner 2 | 3 | StorageCleaner is an extensible declarative disk cleanup mechanism for app-generated data. 4 | 5 | ## Overview 6 | 7 | StorageCleaner allows the app to create and schedule a periodic cleanup job to delete any files created by the app during its execution (e.g. cache or temp files). You can extend the default cleanup mechanism to precisely define conditions related to current storage info (e.g hit cleanup if storage is less than 25%) to hit off a cleanup Job. 8 | 9 | > StorageCleaner uses WorkManager to schedule the cleanup job periodically 10 | 11 | ## Usage 12 | 13 | StorageCleaner comes with a default worker, which deletes the image cache periodically. 14 | 15 | To use the worker add the following code before the end of the application launch sequence, preferably during the app launch sequence, suggested place would be before returning from `application(_:, didFinishLaunchingWithOptions:) -> Bool` 16 | 17 | Create an instance of `StorageCleanerWorker` passing config of protocol type `StorageConfiguration` and an optional protocol type `PerformanceTracker` object, then call `scheduleCleanup()` function on it. 18 | 19 | ```swift 20 | StorageCleanerWorker( 21 | config: StorageCleanerConfiguration.default(), 22 | performanceTracker: nil 23 | ).scheduleCleanup() 24 | ``` 25 | 26 | You can customise the config object by creating a new instance of `StorageCleanerConfiguration` rather than using the default one, to know more about the configs available refer to `StorageCleanerConfiguration` class. 27 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/CleanUpConstraint/CleanUpConstraint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CleanUpConstraint.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Abstract protocol for creating clean up constrainsts 11 | public protocol CleanUpConstraint { 12 | /// Function checks if the current contraint is met and returns true if case met or false in case it does not 13 | /// - Returns: return Bool 14 | func isMet() -> Bool 15 | } 16 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/CleanUpConstraint/ExactStorageSpaceAvailabilityConstraint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExactStorageSpaceAvailabilityConstraint.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Constraint that checks the available disk space against total disk space of device, in terms of provided size in bytes 11 | /// - E.G. Device storage is 100 GB and Free Space is 512 MB 12 | /// - The comparator is passed as <= 13 | /// - The spaceInPercentage is passed as 512 * 1024 * 1024 representing 512 MB of storage 14 | /// - Then when isMet() is call it will yield true 15 | /// - If the Free Space is now 500 MB 16 | /// - It will again yeild true 17 | /// - if the Free Space is now 513 MB 18 | /// - It will yeild false 19 | public final class ExactStorageSpaceAvailabilityConstraint: CleanUpConstraint { 20 | 21 | /// Comparitor of spaceInPercentage against availableSpaceInPercentage you can either pass and closure with float compared with float or you could pass any boolean operation like <=, >=, <,> as comparitors 22 | private var comparator: (_ availableSpaceInBytes: Int64,_ spaceInBytes: Int64) -> Bool 23 | /// The threshold size of space to check the constraint againsts, this size is used to compare against size of space that is currently free in our device 24 | /// e.g pass 512 * 1204 * 1024 for stating that 512 MB of storage should be compared againsts available space size using the provided comparator 25 | private var spaceInBytes: Int64 26 | 27 | private var diskSpaceProvider: DiskSpaceProvider 28 | 29 | /// Creates PercentageStorageSpaceAvailabilityConstraint 30 | /// - Parameters: 31 | /// - comparator: Comparitor of spaceInPercentage against availableSpaceInPercentage you can either pass and closure with float compared with float or you could pass any boolean operation like <=, >=, <,> as comparitors 32 | /// - spaceInBytes: The threshold size of space to check the constraint againsts, this percentage is used to compare against percentage of space that is currently free in our device **in bytes** 33 | /// - diskSpaceProvider: Instance of disk space provider 34 | public init(comparator: @escaping (Int64, Int64) -> Bool, spaceInBytes: Int64, diskSpaceProvider: DiskSpaceProvider) { 35 | self.comparator = comparator 36 | self.spaceInBytes = spaceInBytes 37 | self.diskSpaceProvider = diskSpaceProvider 38 | } 39 | 40 | public func isMet() -> Bool { 41 | let availableSpaceInBytes = (try? diskSpaceProvider.getFreeDiskSpace(forUsageType: .importantUsage)) ?? 0 42 | return comparator(availableSpaceInBytes, spaceInBytes) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/CleanUpConstraint/PercentageStorageSpaceAvailabilityConstraint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PercentageStorageSpaceAvailabilityConstraint.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Constraint that checks the available disk space against total disk space of device, in terms of provided percentage 11 | /// - E.G. Device storage is 100 GB and Free Space is 25 GB 12 | /// - The comparator is passed as <= 13 | /// - The spaceInPercentage is passed as 0.25 representing 25% of storage 14 | /// - Then when isMet() is call it will yield true 15 | /// - If the Free Space is now 24 GB 16 | /// - It will again yeild true 17 | /// - if the Free Space is now 26 GB 18 | /// - It will yeild false 19 | public final class PercentageStorageSpaceAvailabilityConstraint: CleanUpConstraint { 20 | 21 | /// Comparitor of spaceInPercentage against availableSpaceInPercentage you can either pass and closure with float compared with float or you could pass any boolean operation like <=, >=, <,> as comparitors 22 | private var comparator: (_ availableSpaceInPercentage: Float,_ spaceInPercentage: Float) -> Bool 23 | 24 | /// The threshold percentage of space to check the constraint againsts, this percentage is used to compare against percentage of space that is currently free in our device 25 | /// e.g pass 0.25 for stating that 25% of storage should be compared againsts available space percentage using the provided comparator 26 | private var spaceInPercentage: Float 27 | 28 | private var diskSpaceProvider: DiskSpaceProvider 29 | 30 | /// PercentageStorageSpaceAvailabilityConstraint 31 | /// 32 | /// that checks the available disk space against total disk space of device, in terms of provided percentage 33 | /// - Parameters: 34 | /// - comparator: Comparitor of spaceInPercentage against availableSpaceInPercentage you can either pass and closure with float compared with float or you could pass any boolean operation like <=, >=, <,> as comparitors 35 | /// - spaceInPercentage: The threshold percentage of space to check the constraint againsts, this percentage is used to compare against percentage of space that is currently free in our device 36 | /// - diskSpaceProvider: Instance of disk space provider 37 | public init(comparator: @escaping (Float, Float) -> Bool, spaceInPercentage: Float, diskSpaceProvider: DiskSpaceProvider) { 38 | self.comparator = comparator 39 | self.spaceInPercentage = spaceInPercentage 40 | self.diskSpaceProvider = diskSpaceProvider 41 | } 42 | 43 | public func isMet() -> Bool { 44 | let totalSpaceInBytes = (try? diskSpaceProvider.getTotalDiskSpace()) ?? 0 45 | let availableSpaceInBytes = (try? diskSpaceProvider.getFreeDiskSpace(forUsageType: .importantUsage)) ?? 0 46 | let availableSpaceInPercentage = Float(availableSpaceInBytes) / Float (totalSpaceInBytes) 47 | return comparator(availableSpaceInPercentage, spaceInPercentage) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/Cleaner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cleaner.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Cleaner is responsible for get the removable files from provided enevelop and clean the envelop 11 | /// 12 | /// The name of this class was supposed to be StorageCleaner due to issue in Swift compiler which raise name collision we had to change it 13 | /// https://developer.apple.com/forums/thread/690881 14 | /// https://github.com/apple/swift/issues/43510 15 | public final class Cleaner { 16 | 17 | /// Envelope to get the removable files from 18 | private var envelope: StorageEnvelope 19 | /// Receiver to send the removal results to 20 | private var receiver: StorageCleanerResultReceiver 21 | 22 | public init(envelope: StorageEnvelope, receiver: StorageCleanerResultReceiver) { 23 | self.envelope = envelope 24 | self.receiver = receiver 25 | } 26 | 27 | /// Cleans the fetched removable files 28 | public func clean() { 29 | let items = envelope.findRemovableItems() 30 | let result = items.remove() 31 | receiver.receive(result) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/Config/StorageCleanerConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageCleanerConfig.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 18/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents configuration requirements of storage cleaner 11 | public protocol StorageCleanerConfig { 12 | 13 | /// Whether clean-up is enabled. 14 | var isEnabled: Bool { get set } 15 | 16 | /// Lenient clean-up consist of file-age-based deletion of cache files. It is a low-impact, 17 | /// low-risk type of clean-up. So, generally we should be able to run it without any concern 18 | /// to the surrounding storage condition/availability. 19 | /// 20 | /// That being said, this property could be used to control even lenient clean-ups. 21 | /// 22 | /// This `Float` value represents a percentage of remaining available storage space before 23 | /// we'd allow lenient clean-up to proceed. 24 | /// 25 | /// For example, a value of 1.0 effectively means that we'll always allow lenient clean-up 26 | /// to happen every time. (Because available storage space would always be less than 100%). 27 | /// 28 | /// Meanwhile, a value of 0.0 means that lenient operation would never be run. (Because 29 | /// storage space amount couldn't possibly be lesser than 0%.) 30 | var lenientCleanupThresholdInPercentage: Float { get set } 31 | 32 | /// Strict clean-up consist of a more aggressive deletion strategy. It is a high-impact, 33 | /// high-risk clean-up. Unless we're critically low on storage, it's advisable to never 34 | /// run this kind of operation at all. 35 | /// 36 | /// This `Int64` value represents the exact number of remaining available bytes in the 37 | /// storage before we'd allow strict clean-up to happen. 38 | /// 39 | /// A value of `Int64.Magnitude` could be passed to ensure strict clean-up would be run 40 | /// in every opportunities. That said, doing so is not recommended. 41 | var strictCleanupThresholdInBytes: Int64 { get set } 42 | 43 | /// As part of lenient clean-up, we're going to remove old cache files (based on their 44 | /// creation timestamp metadata). 45 | /// 46 | /// This `Int` value represents a day threshold to determine whether a file can 47 | /// be categorized as old or not. 48 | var cacheFileAgeLimitInDays: Int { get set } 49 | 50 | /// As part of strict clean-up, we're going to ensure that the cache directory won't ever 51 | /// cross a particular size limit. 52 | /// 53 | /// This `Int64` value represents that limit (in bytes). 54 | var cacheDirSizeLimitInBytes: Int64 { get set } 55 | 56 | /// Interval in secods for periodic cleanup 57 | var cleanUpInterval: TimeInterval { get set } 58 | 59 | } 60 | 61 | extension StorageCleanerConfig { 62 | 63 | public func toMap() -> [String: String] { 64 | return [ 65 | "is_cleanup_enabled": isEnabled.description, 66 | "lenient_cleanup_threshold_pct": (lenientCleanupThresholdInPercentage * 100).description, 67 | "strict_cleanup_threshold_bytes": 68 | strictCleanupThresholdInBytes.description, 69 | "cache_file_age_limit_days": cacheFileAgeLimitInDays.description, 70 | "cache_dir_size_limit_bytes": cacheDirSizeLimitInBytes.description 71 | ] 72 | } 73 | 74 | var cacheFileAgeLimitInTimeInterval: TimeInterval { 75 | Double(cacheFileAgeLimitInDays) * 24 * 60 * 60 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/Config/StorageCleanerConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageCleanerConfiguration.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 18/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Default storage configuration constants 11 | public struct StorageCleanerDefaultConfig { 12 | /// 25 % of total disk storage available 13 | public static let lenientCleanupThresholdInPercentage: Float = 0.25 14 | /// 512 MB 15 | public static let strictCleanupThresholdInBytes: Int64 = 512 * 1024 * 1024 16 | /// 20 Days 17 | public static let cacheFileAgeLimitInDays: Int = 20 18 | /// 80 MB 19 | public static let cacheDirSizeLimitInBytes: Int64 = 80 * 1024 * 1024 20 | /// 2 Days 21 | public static let cleanUpInterval: TimeInterval = 2 * 24 * 60 * 60 22 | } 23 | 24 | /// Represets default concrete implementation of ``StorageCleanerConfig`` 25 | public struct StorageCleanerConfiguration: StorageCleanerConfig { 26 | 27 | public var isEnabled: Bool = true 28 | 29 | public var lenientCleanupThresholdInPercentage: Float 30 | 31 | public var strictCleanupThresholdInBytes: Int64 32 | 33 | public var cacheFileAgeLimitInDays: Int 34 | 35 | public var cacheDirSizeLimitInBytes: Int64 36 | 37 | public var cleanUpInterval: TimeInterval 38 | 39 | public init(isEnabled: Bool = true, lenientCleanupThresholdInPercentage: Float, strictCleanupThresholdInBytes: Int64, cacheFileAgeLimitInDays: Int, cacheDirSizeLimitInBytes: Int64, cleanUpInterval: TimeInterval) { 40 | self.isEnabled = isEnabled 41 | self.lenientCleanupThresholdInPercentage = lenientCleanupThresholdInPercentage 42 | self.strictCleanupThresholdInBytes = strictCleanupThresholdInBytes 43 | self.cacheFileAgeLimitInDays = cacheFileAgeLimitInDays 44 | self.cacheDirSizeLimitInBytes = cacheDirSizeLimitInBytes 45 | self.cleanUpInterval = cleanUpInterval 46 | } 47 | 48 | /// Returns a StorageCleanerConfiguration with default values from ``StorageCleanerDefaultConfig`` constants 49 | /// - Returns: Object of StorageCleanerConfiguration 50 | public static func `default`() -> StorageCleanerConfiguration { 51 | StorageCleanerConfiguration( 52 | isEnabled: true, 53 | lenientCleanupThresholdInPercentage: StorageCleanerDefaultConfig.lenientCleanupThresholdInPercentage, 54 | strictCleanupThresholdInBytes: StorageCleanerDefaultConfig.strictCleanupThresholdInBytes, 55 | cacheFileAgeLimitInDays: StorageCleanerDefaultConfig.cacheFileAgeLimitInDays, 56 | cacheDirSizeLimitInBytes: StorageCleanerDefaultConfig.cacheDirSizeLimitInBytes, 57 | cleanUpInterval: StorageCleanerDefaultConfig.cleanUpInterval 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/DiskSpaceProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskSpaceProvider.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 16/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Utility class to get device disk space 11 | public class DiskSpaceProvider { 12 | 13 | /// Usage of disk storage to calculate space against 14 | public enum UsageType { 15 | case importantUsage 16 | case opportunisticUsage 17 | } 18 | 19 | private let homeDirectoryPath = NSHomeDirectory() as String 20 | private let homeDirectoryUrl = URL(fileURLWithPath: NSHomeDirectory() as String) 21 | 22 | /// Get total disk space in bytes 23 | /// - Returns: returns total disk space in bytes 24 | public func getTotalDiskSpace() throws -> Int64 { 25 | let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: homeDirectoryPath) 26 | let space = (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value 27 | return space ?? 0 28 | } 29 | 30 | /// Fetch the available according to provided usage tyep 31 | /// - Parameter usageType: usage type of disk space 32 | /// - Returns: returns size 33 | public func getFreeDiskSpace(forUsageType usageType: UsageType) throws -> Int64 { 34 | let size: Int64? 35 | switch usageType { 36 | case .importantUsage: 37 | let resourceKey = URLResourceKey.volumeAvailableCapacityForImportantUsageKey 38 | let resourceValues = try homeDirectoryUrl.resourceValues(forKeys: [resourceKey]) 39 | size = resourceValues.volumeAvailableCapacityForImportantUsage 40 | case .opportunisticUsage: 41 | let resourceKey = URLResourceKey.volumeAvailableCapacityForOpportunisticUsageKey 42 | let resourceValues = try homeDirectoryUrl.resourceValues(forKeys: [resourceKey]) 43 | size = resourceValues.volumeAvailableCapacityForOpportunisticUsage 44 | } 45 | return size ?? 0 46 | } 47 | 48 | /// Get free disk space irrelevent to usage type iOS 13 prior API 49 | /// - Returns: returns size 50 | public func getFreeDiskSpace() throws -> Int64 { 51 | let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: homeDirectoryPath) 52 | let freeSpace = (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value 53 | return freeSpace ?? 0 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/Documentation.docc/GettingStarted.md: -------------------------------------------------------------------------------- 1 | # GettingStarted 2 | 3 | ## Deleting image cache periodically 4 | 5 | StorageCleaner comes with a default worker, which deletes the image cache periodically. 6 | 7 | To use the worker add the following code before the end of the application launch sequence, preferably during the app launch sequence, suggested place would be before returning from `application(_:, didFinishLaunchingWithOptions:) -> Bool` 8 | 9 | Create an instance of `StorageCleanerWorker` passing config of protocol type `StorageConfiguration` and an optional protocol type `PerformanceTracker` object, then call `scheduleCleanup()` function on it. 10 | 11 | ```swift 12 | StorageCleanerWorker( 13 | config: StorageCleanerConfiguration.default(), 14 | performanceTracker: nil 15 | ).scheduleCleanup() 16 | ``` 17 | 18 | You can customise the config object by creating an new instance of `StorageCleanerConfiguration` rather than using the default one, to know more about the configs availble refer to `StorageCleanerConfiguration` class. 19 | 20 | ### Types 21 | 22 | - ``StorageCleaner`` 23 | - ``StorageCleanerWorker`` 24 | - ``StorageCleanerConfig`` 25 | - ``StorageCleanerConfiguration`` 26 | - ``PerformanceTracker`` 27 | 28 | ### Functions 29 | 30 | - ``StorageCleanerWorker/scheduleCleanup()`` 31 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/Documentation.docc/StorageCleaner.md: -------------------------------------------------------------------------------- 1 | # ``StorageCleaner`` 2 | 3 | StorageCleaner is an extensible declarative disk cleanup mechanism for app-generated data. 4 | 5 | ## Overview 6 | 7 | StorageCleaner allows the app to create and schedule a periodic cleanup job to delete any 8 | files created by the app during its execution (e.g. cache or temp files). You can extend the 9 | default cleanup mechanism to precisely define conditions related to current storage info (e.g 10 | hit cleanup if storage is less than 25%) to hit off a cleanup Job. 11 | 12 | ## Topics 13 | 14 | ### Essesntials 15 | 16 | - 17 | - ``StorageCleanerConfiguration`` 18 | - ``StorageCleanerDefaultConfig`` 19 | - ``StorageCleanerWorker`` 20 | - ``CleanUpConstraint`` 21 | - ``StorageEnvelope`` 22 | 23 | ### Defining clean up constraints 24 | 25 | - ``ExactStorageSpaceAvailabilityConstraint`` 26 | - ``PercentageStorageSpaceAvailabilityConstraint`` 27 | 28 | ### Declaring cleanup workflow using envelops 29 | 30 | - ``StorageEnvelopes`` 31 | - ``ConstrainedEnvelopeProxy`` 32 | - ``LegacyStaleFilesEnvelope`` 33 | - ``DirectoryEnvelope`` 34 | 35 | ### Getting current storage informations 36 | 37 | - ``DiskSpaceProvider`` 38 | 39 | ### Logging & Tracking cleanup results 40 | 41 | - ``StorageCleanerResultReceiver`` 42 | - ``AnalyticsResultReceiver`` 43 | - ``DebugConsoleReciever`` 44 | 45 | ### Types 46 | 47 | - ``RemovableItems`` 48 | - ``RemovableFiles`` 49 | - ``EmptyRemovableItems`` 50 | - ``LeastRecentlyModifiedComparator`` 51 | - ``PerformanceTracker`` 52 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/Extensions/URL+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 18/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FileTypeError: Error { 11 | case notADirectory 12 | case resourceValueNotFound 13 | } 14 | 15 | extension URL { 16 | 17 | /// Return the size of content at url, it can be file size or diretory's meta data size 18 | /// - Returns: size of content in bytes 19 | func getSizeOfContent() -> Int64 { 20 | let size = try? FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber 21 | let intSize = Int64(truncating: size ?? 0) 22 | return intSize 23 | } 24 | 25 | /// Returns last access date of that 26 | /// - Returns: Date when the file assicoated with the url was last accessed 27 | func getLastAccessDate() -> Date? { 28 | let resourceValues = try? resourceValues(forKeys: [.contentAccessDateKey]) 29 | return resourceValues?.contentAccessDate 30 | } 31 | 32 | /// Checks if the current URL is of a directory 33 | /// - Returns: return true if is a directory otherwise false 34 | func isDirectory() throws -> Bool { 35 | let resourceKeys = Set([.nameKey, .isDirectoryKey]) 36 | guard let resourceValues = try? resourceValues(forKeys: resourceKeys), 37 | let isDirectory = resourceValues.isDirectory else { 38 | throw FileTypeError.resourceValueNotFound 39 | } 40 | return isDirectory 41 | } 42 | 43 | /// Checks if the current url is of a directory or fails 44 | func assertDirectory() throws { 45 | guard try isDirectory() else { 46 | throw FileTypeError.notADirectory 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/ItemRemovalResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ItemRemovalResult.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public class ItemRemovalResult { 11 | 12 | /// Space free by cleaner in bytes 13 | public var freedSpaceInBytes: Int64 14 | /// Space that was not able to be free after cleanup due to some errors in byte 15 | public var unremovedItemsSpaceInBytes: Int64 16 | /// URL's set that were succesfully removed 17 | public var removedItemUris: Set 18 | /// URL to error map for file that was not able to deleted due to errors 19 | public var unremovedItems: [URL: Error] 20 | 21 | /// Creates ItemRemovalResult with provided attributes 22 | /// - Parameters: 23 | /// - freedSpaceInBytes: Space free by cleaner in bytes 24 | /// - unremovedItemsSpaceInBytes: Space that was not able to be free after cleanup due to some errors in byte 25 | /// - removedItemUris: URL's set that were succesfully removed 26 | /// - unremovedItems: URL to error map for file that was not able to deleted due to errors 27 | public init( 28 | freedSpaceInBytes: Int64, 29 | unremovedItemsSpaceInBytes: Int64, 30 | removedItemUris: Set, 31 | unremovedItems: [URL: Error] 32 | ) { 33 | self.freedSpaceInBytes = freedSpaceInBytes 34 | self.unremovedItemsSpaceInBytes = unremovedItemsSpaceInBytes 35 | self.removedItemUris = removedItemUris 36 | self.unremovedItems = unremovedItems 37 | } 38 | 39 | /// Removed files count 40 | public var removedItemCount: Int { 41 | removedItemUris.count 42 | } 43 | 44 | /// File count that were not able to removed cause of some errors 45 | public var unremovedItemCount: Int { 46 | unremovedItems.count 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/LeastRecentlyModifiedComparator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeastRecentlyModifiedComparator.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 18/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Container struct for comparision function for comparing file according to least recent first basis 11 | public struct LeastRecentlyModifiedComparator { 12 | 13 | /// Compares two fiel url and return true if the lhs was more recently modified than rhs 14 | public static func compare(_ lhs: URL, _ rhs: URL) -> Bool { 15 | let lhsDate = lhs.getLastAccessDate() 16 | let rhsDate = rhs.getLastAccessDate() 17 | switch (lhsDate, rhsDate) { 18 | case let (.some(lhsDate), .some(rhsDate)): 19 | if lhsDate == rhsDate { 20 | return true 21 | } else if lhsDate > rhsDate { 22 | return false 23 | } else { 24 | return true 25 | } 26 | case (.none, .some), 27 | (.some, .none), 28 | (.none,.none): 29 | return true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/PerformanceTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PerformanceTracker.swift 3 | // 4 | // 5 | // Created by Amit Samant on 07/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Represents requirement for tracking performance 11 | public protocol PerformanceTracker { 12 | /// Marks starts of execution of a task to be measured 13 | /// - Parameter identifier: unique identifier for the task 14 | func startMeasuring(identifier: String) 15 | /// Marks end of execution of a task to be measured 16 | /// - Parameter identifier: unique identifier for the task 17 | func stopMeasuring(identifier: String) 18 | /// Add attributes associated to a particular task 19 | /// - Parameters: 20 | /// - identifier: unique identifier for the task 21 | /// - attributes: attributes hashmap 22 | func addAttributes(identifier: String, attributes: [String: String]) 23 | /// Adds metrics associated to a particular task 24 | /// - Parameters: 25 | /// - identifier: unique identifier for the task 26 | /// - attributes: attributes hashmap 27 | func addMetrics(identifier: String, metrics: [String: Int64]) 28 | } 29 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/RemovableItems/EmptyRemovableItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyRemovableItems.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Empty concrete implementation of RemovableItems 11 | public final class EmptyRemovableItems: RemovableItems { 12 | 13 | public var count: Int { 0 } 14 | public var sizeInBytes: Int64 { 0 } 15 | 16 | public func trimToSize(bytes: Int64) -> RemovableItems { 17 | return EmptyRemovableItems() 18 | } 19 | 20 | public func dryRun() -> ItemRemovalResult { 21 | ItemRemovalResult( 22 | freedSpaceInBytes: 0, 23 | unremovedItemsSpaceInBytes: 0, 24 | removedItemUris: Set(), 25 | unremovedItems: [:] 26 | ) 27 | } 28 | 29 | public func remove() -> ItemRemovalResult { 30 | ItemRemovalResult( 31 | freedSpaceInBytes: 0, 32 | unremovedItemsSpaceInBytes: 0, 33 | removedItemUris: Set(), 34 | unremovedItems: [:] 35 | ) 36 | } 37 | 38 | public func sort(comparator: (URL, URL) -> Bool) -> RemovableItems { 39 | EmptyRemovableItems() 40 | } 41 | 42 | public func plus(other: RemovableItems) -> RemovableItems { 43 | EmptyRemovableItems() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/RemovableItems/RemovableFiles.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemovableFiles.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Removable file is an concrete implementation of RemovableItems having url base files as the deletable items 11 | public final class RemovableFiles: RemovableItems { 12 | 13 | /// Urls of files 14 | var fileURls: [URL] 15 | 16 | /// Count of elligible files 17 | public var count: Int { fileURls.count } 18 | lazy public var sizeInBytes: Int64 = { 19 | return fileURls.reduce(0) { partialResult, url -> Int64 in 20 | let fileSize = url.getSizeOfContent() 21 | return partialResult + fileSize 22 | } 23 | }() 24 | 25 | /// RemovableFiles init 26 | /// 27 | /// Removable file is an concrete implementation of RemovableItems having url base files as the deletable items 28 | /// - Parameter files: file urls 29 | public init(files: [URL]) { 30 | self.fileURls = Array(Set(files)) 31 | } 32 | 33 | public func sort(comparator: (URL, URL) -> Bool) -> RemovableItems { 34 | let sortedFiles = fileURls.sorted(by: comparator) 35 | return RemovableFiles(files: sortedFiles) 36 | } 37 | 38 | public func trimToSize(bytes sizeInBytes: Int64) -> RemovableItems { 39 | guard sizeInBytes > 1 else { 40 | return EmptyRemovableItems() 41 | } 42 | var count = 0 43 | var size: Int64 = 0 44 | for url in fileURls { 45 | if size >= sizeInBytes { 46 | break 47 | } 48 | let fileSize = url.getSizeOfContent() 49 | size += fileSize 50 | count += 1 51 | } 52 | return RemovableFiles(files: Array(fileURls[0.. ItemRemovalResult { 56 | return commit(shouldActuallyDelete: true) 57 | } 58 | 59 | public func dryRun() -> ItemRemovalResult { 60 | return commit(shouldActuallyDelete: false) 61 | } 62 | 63 | /// Deletes the file depending on `shouldActuallyDelete` vauleu and genrates the removal result 64 | /// - Parameter shouldActuallyDelete: if the action should commited or should just calculate the size that could have freed if deleted 65 | /// - Returns: removal result 66 | func commit(shouldActuallyDelete: Bool) -> ItemRemovalResult { 67 | var removedUrls = Set() 68 | var unremovedItemsSpaceInBytes: Int64 = 0 69 | var unremovedUrlsToCause = [URL: Error]() 70 | var removableSizeInBytes: Int64 = 0 71 | 72 | fileURls.forEach { fileURL in 73 | let fileSize = fileURL.getSizeOfContent() 74 | 75 | if shouldActuallyDelete { 76 | let path = fileURL.path 77 | do { 78 | try FileManager.default.removeItem(atPath: path) 79 | removedUrls.insert(fileURL) 80 | removableSizeInBytes += fileSize 81 | } catch { 82 | unremovedUrlsToCause[fileURL] = error 83 | unremovedItemsSpaceInBytes += fileSize 84 | } 85 | } else { 86 | removedUrls.insert(fileURL) 87 | removableSizeInBytes += fileSize 88 | } 89 | } 90 | 91 | return ItemRemovalResult( 92 | freedSpaceInBytes: removableSizeInBytes, 93 | unremovedItemsSpaceInBytes: unremovedItemsSpaceInBytes, 94 | removedItemUris: removedUrls, 95 | unremovedItems: unremovedUrlsToCause 96 | ) 97 | } 98 | 99 | public func plus(other: RemovableItems) -> RemovableItems { 100 | guard let otherRemovableFiles = other as? RemovableFiles else { 101 | return self 102 | } 103 | let mergedFileURL = fileURls + otherRemovableFiles.fileURls 104 | let setOfMergedFileURL = Set(mergedFileURL) 105 | return RemovableFiles(files: Array(setOfMergedFileURL)) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/RemovableItems/RemovableItems.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemovableItems.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Protocol encapsulating items that are removable 11 | public protocol RemovableItems { 12 | 13 | /// counts of removable items 14 | var count: Int { get } 15 | 16 | /// Commulative size of all removable 17 | var sizeInBytes: Int64 { get } 18 | 19 | /// Responsible to return sorted variant of removable items, according to the implementation of concrete type 20 | /// - Returns: sorted variant of removable items 21 | func sort(comparator: (URL, URL) -> Bool) -> RemovableItems 22 | 23 | /// Responsible to return trimmed varient of removable item, accroding to the implementation of concrete type 24 | /// - Returns: trimmed varient of removable item 25 | func trimToSize(bytes: Int64) -> RemovableItems 26 | 27 | /// Dry run the clean up and genrates the removal result but do not perform actual clean up, accroding to the implementation of concrete type 28 | /// - Returns: removal result 29 | func dryRun() -> ItemRemovalResult 30 | 31 | /// Clean/Deletes the remivable items and return removal result, accroding to the implementation of concrete type 32 | /// - Returns: removal result 33 | func remove() -> ItemRemovalResult 34 | 35 | /// Combine provided removable result with current instance, accroding to the implementation of concrete type 36 | /// - Returns: removal result 37 | func plus(other: RemovableItems) -> RemovableItems 38 | } 39 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageCleanerResultReceiver/AnalyticsResultReceiver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnalyticsResultReceiver.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 23/02/22. 6 | // Copyright © 2022 PT GoJek Indonesia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// AnalyticsResultReceiver is StorageCleanerResultReceiver's concerete implementation which is responsible to report removal result data points to provided performance tracker 12 | public final class AnalyticsResultReceiver: StorageCleanerResultReceiver { 13 | 14 | private let storageCleanerTrackingKey = "Disk Cleaner" 15 | private let freedSpaceTrackingKey = "Disk Cleaner: Freed Space" 16 | private let unremovedSpaceTrackingKey = "Disk Cleaner: Unremoved Space" 17 | private let removedItemCountTrackingKey = "Disk Cleaner: Removed Count" 18 | private let unremovedItemCountTrackingKey = "Disk Cleaner: Unremoved Count" 19 | 20 | private let performanceTracker: PerformanceTracker 21 | private let attributesMapProvider: (() -> [String: String])? 22 | 23 | /// AnalyticsResultReceiver init 24 | /// 25 | /// AnalyticsResultReceiver is StorageCleanerResultReceiver's concerete implementation which is responsible to report removal result data points to provided performance tracker 26 | /// - Parameter performanceTracker: performace tracker where the perfomance metric should be reported to 27 | public init?(performanceTracker: PerformanceTracker?, attributesMapProvider: (() -> [String: String])? = nil) { 28 | guard let performanceTracker = performanceTracker else { 29 | return nil 30 | } 31 | self.performanceTracker = performanceTracker 32 | self.attributesMapProvider = attributesMapProvider 33 | } 34 | 35 | public func receive(_ result: ItemRemovalResult) { 36 | performanceTracker.startMeasuring(identifier: storageCleanerTrackingKey) 37 | performanceTracker.addMetrics( 38 | identifier: storageCleanerTrackingKey, 39 | metrics: [ 40 | freedSpaceTrackingKey: result.freedSpaceInBytes, 41 | unremovedSpaceTrackingKey: result.unremovedItemsSpaceInBytes, 42 | removedItemCountTrackingKey: Int64(result.removedItemCount), 43 | unremovedItemCountTrackingKey: Int64(result.unremovedItemCount) 44 | ] 45 | ) 46 | if let attributesMapProvider = attributesMapProvider { 47 | let attributes = attributesMapProvider() 48 | performanceTracker.addAttributes( 49 | identifier: storageCleanerTrackingKey, 50 | attributes: attributes 51 | ) 52 | } 53 | performanceTracker.stopMeasuring(identifier: storageCleanerTrackingKey) 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageCleanerResultReceiver/DebugConsoleReciever.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugConsoleReciever.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 18/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// DebugConsoleReciever is resposible to print the removal result info to debug console 11 | public final class DebugConsoleReciever: StorageCleanerResultReceiver { 12 | public func receive(_ result: ItemRemovalResult) { 13 | let string = """ 14 | Removed \(result.removedItemCount) item(s) 15 | worth \(result.freedSpaceInBytes)) 16 | \(result.unremovedItemCount) failed to be removed.\n reason map: \(result.unremovedItems) 17 | """ 18 | debugPrint(string) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageCleanerResultReceiver/StorageCleanerResultReceiver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageCleanerResultReceiver.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Protocol representing the cleaner result receiver 11 | public protocol StorageCleanerResultReceiver { 12 | func receive(_ result: ItemRemovalResult) 13 | } 14 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageCleanerResultReceiver/StorageCleanerResultReceivers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageCleanerResultReceivers.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 15/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Composing implamentation which combine two or more recivers into a new combined reciever 11 | public final class StorageCleanerResultReceivers: StorageCleanerResultReceiver { 12 | 13 | /// Backing recivers to report the cleaner result to 14 | private var delegates: [StorageCleanerResultReceiver] 15 | 16 | /// StorageCleanerResultReceivers init 17 | /// - Parameter delegates: Backing recivers to report the cleaner result to 18 | public init(delegates: [StorageCleanerResultReceiver]) { 19 | self.delegates = delegates 20 | } 21 | 22 | /// StorageCleanerResultReceivers variadic init 23 | /// - Parameter delegates: Backing recivers to report the cleaner result to 24 | public init(_ delegates: StorageCleanerResultReceiver?...) { 25 | self.delegates = delegates.compactMap { $0 } 26 | } 27 | 28 | public func receive(_ result: ItemRemovalResult) { 29 | delegates.forEach { 30 | $0.receive(result) 31 | } 32 | } 33 | } 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageCleanerWorker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageCleanerWorker.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 18/02/22. 6 | // 7 | 8 | import Foundation 9 | import WorkManager 10 | 11 | public typealias FileFilter = (_ fileURL: URL) -> Bool 12 | 13 | /// Responsible to schedule cleanup job periodically to delete image caches 14 | public class StorageCleanerWorker { 15 | 16 | private let config: StorageCleanerConfig 17 | private let performanceTracker: PerformanceTracker? 18 | private let storageCleanerPerformanceTrackingKey = "Disk Cleaner Performance" 19 | private let storageCleanerTaskKey = "disk_cleaner" 20 | private let excludedFileList = ["Cache.db", "Cache.db-wal", "Cache.db-shm"] 21 | private let cacheRootDirectory: URL? 22 | 23 | /// Creates PercentageStorageSpaceAvailabilityConstraint 24 | /// - Parameters: 25 | /// - config: StorageCleanerConfig comforming object 26 | /// - performanceTracker: PerformanceTracker comforming object 27 | /// - cacheRootDirectory: optional cacheRootDirectory description 28 | public init( 29 | config: StorageCleanerConfig, 30 | performanceTracker: PerformanceTracker?, 31 | cacheRootDirectory: URL? = nil 32 | ) { 33 | self.config = config 34 | self.performanceTracker = performanceTracker 35 | self.cacheRootDirectory = cacheRootDirectory 36 | } 37 | 38 | private var fsCahceURL: URL? { 39 | let systemDefaultCacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first 40 | guard let cachesDirectory = cacheRootDirectory ?? systemDefaultCacheDirectory else { 41 | return nil 42 | } 43 | return cachesDirectory 44 | .appendingPathComponent("ImageDownloadCache") 45 | .appendingPathComponent("fsCachedData") 46 | } 47 | 48 | // Exclude the designated file from rempvable items list 49 | private func excludingFilesFilter(_ file: URL) -> Bool { 50 | return excludedFileList.contains(file.lastPathComponent) == false 51 | } 52 | 53 | /// Performs the cleanup based on ``StorageCleanerConfig`` and storage info from ``DiskSpaceProvider`` 54 | public func doWork() { 55 | // Tracks the performance of clean up task 56 | performanceTracker?.startMeasuring(identifier: storageCleanerPerformanceTrackingKey) 57 | defer { 58 | performanceTracker?.stopMeasuring(identifier: storageCleanerPerformanceTrackingKey) 59 | } 60 | 61 | guard let cachesDirectory = fsCahceURL else { 62 | return 63 | } 64 | 65 | let diskSpaceProvider = DiskSpaceProvider() 66 | 67 | do { 68 | // This constraint only allows call to delegate enevolope if the current available size is less than or equal to the threshold percentage, other wise ConstrainedEnvelopeProxy returns empty removable items 69 | let envelope = ConstrainedEnvelopeProxy( 70 | constraint: PercentageStorageSpaceAvailabilityConstraint( 71 | comparator: <=, 72 | spaceInPercentage: config.lenientCleanupThresholdInPercentage, 73 | diskSpaceProvider: diskSpaceProvider 74 | ), 75 | 76 | // This envelopes get asked for removable items if the above contraint is met, this envelop contains sub envelop and when the removable item is called upon this it merges the result from sub enveloped 77 | delegate: StorageEnvelopes( 78 | 79 | // This envelop fetches the stale files which are not used in the a set time duration 80 | try LegacyStaleFilesEnvelope( 81 | directoryURL: cachesDirectory, 82 | filter: excludingFilesFilter, 83 | thresholdTimeInterval: config.cacheFileAgeLimitInTimeInterval 84 | ), 85 | 86 | // This constraint only allows call to delegate evenloe if the current avaiaable space is <= to the space in byte provided 87 | ConstrainedEnvelopeProxy( 88 | constraint: ExactStorageSpaceAvailabilityConstraint( 89 | comparator: <=, 90 | spaceInBytes: config.strictCleanupThresholdInBytes, 91 | diskSpaceProvider: diskSpaceProvider 92 | ), 93 | // This envelope gets the exact amount of removable file if available after deleting which the freed size will be the provided size limit 94 | delegate: try DirectoryEnvelope( 95 | directoryURL: cachesDirectory, 96 | filter: excludingFilesFilter, 97 | fileComparator: LeastRecentlyModifiedComparator.compare, 98 | sizeLimitInBytes: config.cacheDirSizeLimitInBytes 99 | ) 100 | ) 101 | ) 102 | ) 103 | let receiver = StorageCleanerResultReceivers( 104 | DebugConsoleReciever(), 105 | AnalyticsResultReceiver(performanceTracker: performanceTracker) 106 | ) 107 | let cleaner = Cleaner(envelope: envelope, receiver: receiver) 108 | cleaner.clean() 109 | } catch { 110 | debugPrint(error) 111 | } 112 | } 113 | 114 | /// Schedules the periodic cleanup job 115 | public func scheduleCleanup() { 116 | WorkManager.shared.enqueueUniquePeriodicWork( 117 | id: storageCleanerTaskKey, 118 | interval: config.cleanUpInterval, 119 | work: doWork, 120 | doesNotPerformOnLowPower: true, 121 | onQueue: DispatchQueue.global(qos: .background) 122 | ) 123 | } 124 | 125 | /// Cancel already scheduled cleanup job 126 | public func cancelScheduledCleanup() { 127 | WorkManager.shared.cancelQueuedPeriodicWork(withId: storageCleanerTaskKey) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageEnvelope/ConstrainedEnvelopeProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConstrainedEnvelopeProxy.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// ConstrainedEnvelopeProxy acts as a proxy which check for of provided constraint is met or not and prevent or execute the call to the delegate if the constraint is met or not 11 | public final class ConstrainedEnvelopeProxy: StorageEnvelope { 12 | 13 | /// Clean up contraint to enforce 14 | private var constraint: CleanUpConstraint 15 | /// Storage envelope to delegate to 16 | private var delegate: StorageEnvelope 17 | 18 | /// ConstrainedEnvelopeProxy initialiser 19 | /// Acts as a proxy which check for of provided constraint is met or not and prevent or execute the call to the delegate if the constraint is met or not 20 | /// - Parameters: 21 | /// - constraint: Clean up contraint to enforce 22 | /// - delegate: Storgae envelope to delegate to 23 | public init(constraint: CleanUpConstraint, delegate: StorageEnvelope) { 24 | self.constraint = constraint 25 | self.delegate = delegate 26 | } 27 | 28 | public func findRemovableItems() -> RemovableItems { 29 | if (constraint.isMet()) { 30 | return delegate.findRemovableItems() 31 | } else { 32 | return EmptyRemovableItems() 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageEnvelope/DirectoryEnvelope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryEnvelope.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Directory Enveloper is responsible to collect removable files from provioded directory 11 | public final class DirectoryEnvelope: StorageEnvelope { 12 | 13 | /// Directory to fetchf file from, this directory is traversed in depth first fashion so the file included are also from relative subdirectories 14 | private var directoryURL: URL 15 | /// File fileter to apply on collected file 16 | private var filter: FileFilter? = nil 17 | /// File comparitor to sort the files 18 | private var fileComparator: ((URL, URL) -> Bool)? 19 | /// Size limit to trim the file limit 20 | private var sizeLimitInBytes: Int64? = nil 21 | 22 | /// Creates a new DirectoryEnvelope 23 | /// - Parameters: 24 | /// - directoryURL: Directory to fetchf file from, this directory is traversed in depth first fashion so the file included are also from relative subdirectories 25 | /// - filter: File fileter to apply on collected file 26 | /// - fileComparator: File comparitor to sort the files 27 | /// - sizeLimitInBytes: Size limit to trim the file limit 28 | public init(directoryURL: URL, filter: FileFilter? = nil, fileComparator: ((URL, URL) -> Bool)?, sizeLimitInBytes: Int64? = nil) throws { 29 | try directoryURL.assertDirectory() 30 | self.directoryURL = directoryURL 31 | self.filter = filter 32 | self.fileComparator = fileComparator 33 | self.sizeLimitInBytes = sizeLimitInBytes 34 | } 35 | 36 | public func findRemovableItems() -> RemovableItems { 37 | guard let directoryEnumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey]) else { 38 | return EmptyRemovableItems() 39 | } 40 | 41 | let fileURLs = directoryEnumerator.compactMap{ (content: Any) -> URL? in 42 | let resourceKeys = Set([.nameKey, .isDirectoryKey]) 43 | guard let contentURL = content as? URL, 44 | let resourceValues = try? contentURL.resourceValues(forKeys: resourceKeys), 45 | let isDirectory = resourceValues.isDirectory, isDirectory == false else { 46 | return nil 47 | } 48 | if let filter = filter, filter(contentURL) == false { 49 | return nil 50 | } 51 | return contentURL 52 | } 53 | 54 | let files = RemovableFiles(files: fileURLs) 55 | 56 | let sorted: RemovableItems 57 | if let comparator = fileComparator { 58 | sorted = files.sort(comparator: comparator) 59 | } else { 60 | sorted = files 61 | } 62 | 63 | var trimmed: RemovableItems 64 | if let sizeLimitInBytes = sizeLimitInBytes { 65 | let sizeToRemove = max((files.sizeInBytes - sizeLimitInBytes),0) 66 | trimmed = sorted.trimToSize(bytes: sizeToRemove) 67 | } else { 68 | trimmed = sorted 69 | } 70 | 71 | return trimmed 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageEnvelope/LegacyStaleFilesEnvelope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LegacyStaleFilesEnvelope.swift 3 | // Launchpad 4 | // 5 | // Created by Amit Samant on 21/02/22. 6 | // Copyright © 2022 PT GoJek Indonesia. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// This envelope is responsible for fetching stale file from provided directory 12 | public final class LegacyStaleFilesEnvelope: StorageEnvelope { 13 | 14 | /// Directory url to retrieve files from 15 | private var directoryURL: URL 16 | /// File filter to apply on files, use this to exclude any files from cleanup 17 | private var filter: FileFilter? = nil 18 | /// Time interval after which a file is considered stale 19 | private var thresholdTimeInterval: TimeInterval 20 | 21 | /// Create a new LegacyStaleFilesEnvelope 22 | /// - Parameters: 23 | /// - directoryURL: Directory url to retrieve files from 24 | /// - filter: File filter to apply on files, use this to exclude any files from cleanup 25 | /// - thresholdTimeInterval: Time interval after which a file is considered stale 26 | public init(directoryURL: URL, filter: FileFilter? = nil, thresholdTimeInterval: TimeInterval) throws { 27 | try directoryURL.assertDirectory() 28 | self.directoryURL = directoryURL 29 | self.filter = filter 30 | self.thresholdTimeInterval = thresholdTimeInterval 31 | } 32 | 33 | public func findRemovableItems() -> RemovableItems { 34 | guard let directoryEnumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [URLResourceKey.isDirectoryKey]) else { 35 | return EmptyRemovableItems() 36 | } 37 | 38 | let fileURLs = directoryEnumerator.compactMap{ (content: Any) -> URL? in 39 | let resourceKeys = Set([.nameKey, .isDirectoryKey]) 40 | guard let contentURL = content as? URL, 41 | let resourceValues = try? contentURL.resourceValues(forKeys: resourceKeys), 42 | let isDirectory = resourceValues.isDirectory, 43 | isDirectory == false, 44 | isStale(contentURL) == true else { 45 | return nil 46 | } 47 | if let filter = filter, filter(contentURL) == false { 48 | return nil 49 | } 50 | return contentURL 51 | } 52 | 53 | let files = RemovableFiles(files: fileURLs) 54 | return files 55 | } 56 | 57 | /// Checks the last accessed time vs the current time time diffrence and compare that with provided time interval to check if the file is stale 58 | /// - Parameter fileURL: url of file 59 | /// - Returns: returns true if file is stale otherwise false 60 | private func isStale(_ fileURL: URL) -> Bool { 61 | guard let fileLastAccessDate = fileURL.getLastAccessDate() else { 62 | return false 63 | } 64 | let diff = abs(Date().timeIntervalSince(fileLastAccessDate)) 65 | return diff >= thresholdTimeInterval 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageEnvelope/StorageEnvelope.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageEnvelope.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Protocol encapsulating an directory enevelope resposible to provide removable items 11 | public protocol StorageEnvelope { 12 | /// Returns the removable items, according to the implementation provided by the conforming concrete implementation 13 | /// - Returns: returns RemovableItems 14 | func findRemovableItems() -> RemovableItems 15 | } 16 | -------------------------------------------------------------------------------- /StorageCleaner/Sources/StorageEnvelope/StorageEnvelopes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageEnvelopes.swift 3 | // StorageCleaner 4 | // 5 | // Created by Amit Samant on 17/02/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// An composing implementation of StorageEnvelope which combine two or more enevelopes into a combine envelop 11 | public final class StorageEnvelopes: StorageEnvelope { 12 | 13 | /// backing envelopes to be combine 14 | private var envelopes: [StorageEnvelope] 15 | 16 | /// Variadic init for StorageEnvelopes 17 | /// 18 | /// An composing implementation of StorageEnvelope which combine two or more enevelopes into a combine envelop 19 | /// - Parameter envelopes:envelopes to be combine 20 | public init(_ envelopes: StorageEnvelope...) { 21 | self.envelopes = envelopes 22 | } 23 | 24 | /// StorageEnvelopes init 25 | /// 26 | /// An composing implementation of StorageEnvelope which combine two or more enevelopes into a combine envelop 27 | /// - Parameter envelopes:envelopes to be combine 28 | public init(envelopes: [StorageEnvelope]) { 29 | self.envelopes = envelopes 30 | } 31 | 32 | public func findRemovableItems() -> RemovableItems { 33 | let removableItemsList = envelopes.map { $0.findRemovableItems() } 34 | guard let first = removableItemsList.first else { 35 | return EmptyRemovableItems() 36 | } 37 | let otherArray = removableItemsList[1.. RemovableItems in 39 | return first.plus(other: next) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /StorageCleaner/Tests/StorageCleanerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import StorageCleaner 3 | 4 | final class StorageCleanerTests: XCTestCase { 5 | 6 | // Placeholder test 7 | func testExample() throws { 8 | assert(true) 9 | } 10 | } 11 | --------------------------------------------------------------------------------