A lightweight generic cache for iOS written in Swift with extra love for images.

Related tags

Cache HanekeSwift
Overview

Haneke

Carthage compatible SwiftPM compatible Accio supported Platform Build Status Join the chat at https://gitter.im/Haneke/HanekeSwift

Haneke is a lightweight generic cache for iOS and tvOS written in Swift 4. It's designed to be super-simple to use. Here's how you would initalize a JSON cache and fetch objects from a url:

let cache = Cache<JSON>(name: "github")
let URL = NSURL(string: "https://api.github.com/users/haneke")!

cache.fetch(URL: URL).onSuccess { JSON in
    print(JSON.dictionary?["bio"])
}

Haneke provides a memory and LRU disk cache for UIImage, NSData, JSON, String or any other type that can be read or written as data.

Particularly, Haneke excels at working with images. It includes a zero-config image cache with automatic resizing. Everything is done in background, allowing for fast, responsive scrolling. Asking Haneke to load, resize, cache and display an appropriately sized image is as simple as:

imageView.hnk_setImageFromURL(url)

Really.

Features

  • Generic cache with out-of-the-box support for UIImage, NSData, JSON and String
  • First-level memory cache using NSCache
  • Second-level LRU disk cache using the file system
  • Asynchronous fetching of original values from network or disk
  • All disk access is performed in background
  • Thread-safe
  • Automatic cache eviction on memory warnings or disk capacity reached
  • Comprehensive unit tests
  • Extensible by defining custom formats, supporting additional types or implementing custom fetchers

For images:

  • Zero-config UIImageView and UIButton extensions to use the cache, optimized for UITableView and UICollectionView cell reuse
  • Background image resizing and decompression

Installation

Using CocoaPods:

use_frameworks!
pod 'HanekeSwift'

Using Carthage:

github "Haneke/HanekeSwift"

Using SwiftPM or Accio:

.package(url: "https://github.com/Haneke/HanekeSwift.git", .upToNextMajor(from: "0.11.2")),

Then link Haneke in your App target like so:

.target(
    name: "App",
    dependencies: [
        "Haneke",
    ]
),

Manually:

  1. Drag Haneke.xcodeproj to your project in the Project Navigator.
  2. Select your project and then your app target. Open the Build Phases panel.
  3. Expand the Target Dependencies group, and add Haneke.framework.
  4. Click on the + button at the top left of the panel and select New Copy Files Phase. Set Destination to Frameworks, and add Haneke.framework.
  5. import Haneke whenever you want to use Haneke.

Requirements

  • iOS 8.0+ or tvOS 9.1+
  • Swift 4

Using the cache

Haneke provides shared caches for UIImage, NSData, JSON and String. You can also create your own caches.

The cache is a key-value store. For example, here's how you would cache and then fetch some data.

let cache = Shared.dataCache

cache.set(value: data, key: "funny-games.mp4")

// Eventually...

cache.fetch(key: "funny-games.mp4").onSuccess { data in
    // Do something with data
}

In most cases the value will not be readily available and will have to be fetched from network or disk. Haneke offers convenience fetch functions for these cases. Let's go back to the first example, now using a shared cache:

let cache = Shared.JSONCache
let URL = NSURL(string: "https://api.github.com/users/haneke")!

cache.fetch(URL: URL).onSuccess { JSON in
   print(JSON.dictionary?["bio"])
}

The above call will first attempt to fetch the required JSON from (in order) memory, disk or NSURLCache. If not available, Haneke will fetch the JSON from the source, return it and then cache it. In this case, the URL itself is used as the key.

Further customization can be achieved by using formats, supporting additional types or implementing custom fetchers.

Extra ♡ for images

Need to cache and display images? Haneke provides convenience methods for UIImageView and UIButton with optimizations for UITableView and UICollectionView cell reuse. Images will be resized appropriately and cached in a shared cache.

// Setting a remote image
imageView.hnk_setImageFromURL(url)

// Setting an image manually. Requires you to provide a key.
imageView.hnk_setImage(image, key: key)

The above lines take care of:

  1. If cached, retrieving an appropriately sized image (based on the bounds and contentMode of the UIImageView) from the memory or disk cache. Disk access is performed in background.
  2. If not cached, loading the original image from web/memory and producing an appropriately sized image, both in background. Remote images will be retrieved from the shared NSURLCache if available.
  3. Setting the image and animating the change if appropriate.
  4. Or doing nothing if the UIImageView was reused during any of the above steps.
  5. Caching the resulting image.
  6. If needed, evicting the least recently used images in the cache.

Formats

Formats allow to specify the disk cache size and any transformations to the values before being cached. For example, the UIImageView extension uses a format that resizes images to fit or fill the image view as needed.

You can also use custom formats. Say you want to limit the disk capacity for icons to 10MB and apply rounded corners to the images. This is how it could look like:

let cache = Shared.imageCache

let iconFormat = Format<UIImage>(name: "icons", diskCapacity: 10 * 1024 * 1024) { image in
    return imageByRoundingCornersOfImage(image)
}
cache.addFormat(iconFormat)

let URL = NSURL(string: "http://haneke.io/icon.png")!
cache.fetch(URL: URL, formatName: "icons").onSuccess { image in
    // image will be a nice rounded icon
}

Because we told the cache to use the "icons" format Haneke will execute the format transformation in background and return the resulting value.

Formats can also be used from the UIKit extensions:

imageView.hnk_setImageFromURL(url, format: iconFormat)

Fetchers

The fetch functions for urls and paths are actually convenience methods. Under the hood Haneke uses fetcher objects. To illustrate, here's another way of fetching from a url by explictly using a network fetcher:

let URL = NSURL(string: "http://haneke.io/icon.png")!
let fetcher = NetworkFetcher<UIImage>(URL: URL)
cache.fetch(fetcher: fetcher).onSuccess { image in
    // Do something with image
}

Fetching an original value from network or disk is an expensive operation. Fetchers act as a proxy for the value, and allow Haneke to perform the fetch operation only if absolutely necessary.

In the above example the fetcher will be executed only if there is no value associated with "http://haneke.io/icon.png" in the memory or disk cache. If that happens, the fetcher will be responsible from fetching the original value, which will then be cached to avoid further network activity.

Haneke provides two specialized fetchers: NetworkFetcher<T> and DiskFetcher<T>. You can also implement your own fetchers by subclassing Fetcher<T>.

Custom fetchers

Through custom fetchers you can fetch original values from other sources than network or disk (e.g., Core Data), or even change how Haneke acceses network or disk (e.g., use Alamofire for networking instead of NSURLSession). A custom fetcher must subclass Fetcher<T> and is responsible for:

  • Providing the key (e.g., NSURL.absoluteString in the case of NetworkFetcher) associated with the value to be fetched
  • Fetching the value in background and calling the success or failure closure accordingly, both in the main queue
  • Cancelling the fetch on demand, if possible

Fetchers are generic, and the only restriction on their type is that it must implement DataConvertible.

Supporting additional types

Haneke can cache any type that can be read and saved as data. This is indicated to Haneke by implementing the protocols DataConvertible and DataRepresentable.

public protocol DataConvertible {
    typealias Result

    class func convertFromData(data:NSData) -> Result?

}

public protocol DataRepresentable {

    func asData() -> NSData!

}

This is how one could add support for NSDictionary:

extension NSDictionary : DataConvertible, DataRepresentable {

    public typealias Result = NSDictionary

    public class func convertFromData(data:NSData) -> Result? {
        return NSKeyedUnarchiver.unarchiveObjectWithData(data) as? NSDictionary
    }

    public func asData() -> NSData! {
        return NSKeyedArchiver.archivedDataWithRootObject(self)
    }

}

Then creating a NSDictionary cache would be as simple as:

let cache = Cache<NSDictionary>(name: "dictionaries")

Roadmap

Haneke Swift is in initial development and its public API should not be considered stable.

License

Copyright 2014 Hermes Pique (@hpique)
                 2014 Joan Romano (@joanromano)
                 2014 Luis Ascorbe (@lascorbe)
                 2014 Oriol Blanc (@oriolblanc)

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Comments
  • EXC_BAD_ACCESS in UIImage+Haneke.swift

    EXC_BAD_ACCESS in UIImage+Haneke.swift

    Line 56:

    if let context = CGBitmapContextCreate(nil, UInt(pixelSize.width), UInt(pixelSize.height), CGImageGetBitsPerComponent(originalImageRef), 0, colorSpace, bitmapInfo) {
    

    This is happening during app startup in simulator... run it again, and it usually works. Seen it several times now. XCode 6.1, iOS 8.1.

    Will try to post more info + trace next time I see it.

    bug 
    opened by SteveNewhouse 48
  • Support iOS7

    Support iOS7

    Swift apps can run on iOS7. It'd be nice if I could still use HanekeSwift on iOS7 too. Obviously iOS7 doesn't support frameworks so including Haneke in my app won't be as simple, but is there any reason Haneke wouldn't work on iOS7? Or you using (or planning to use) iOS8 specific APIs?

    enhancement help wanted question 
    opened by jmacmullin 33
  • Caching file, returning disk URL to retrieve later

    Caching file, returning disk URL to retrieve later

    Hey there!

    I have a requirement to cache video data, but then retrieve the URL location for that data. The reason for this is an AVAsset cannot be created using NSData, only via a URL.

    A very hacky (and very unreliable) way to do it now via Haneke is to do the following:

    let cache = Haneke.sharedDataCache
    cache.fetch(URL: URL).onSuccess { (data) in
      // Hacky way of getting cache URL from Haneke
      let path = DiskCache(name: "shared-data", capacity: UINT64_MAX).pathForKey(URL.absoluteString!)
      let cacheURL = NSURL(fileURLWithPath: path)
    }
    

    What are your thoughts on how this should be handled? Should I forego using Haneke for this specific feature and build a disk-only cache instead?

    James

    question 
    opened by jamescmartinez 21
  • Issue #115 - Synchronise access to UIImage with data to avoid crash

    Issue #115 - Synchronise access to UIImage with data to avoid crash

    It seems that UIImage with data constructor is not thread safe. Having two simultaneous threads accessing it was creating the Issue #115 reported already by many. Added NSLock to synchronise access between the several threads…

    opened by paulosotu 18
  • Swift 1.2 compile issues

    Swift 1.2 compile issues

    Lots of compile issues with Swift 1.2 (not unique to this library, FWIW). Tried the auto-convert to 1.2 and that did not resolve them all.

    Tried fixing other manually but not familiar enough with the 1.2 migration issues to work through it.

    enhancement 
    opened by SteveNewhouse 13
  • Swift 3 Support

    Swift 3 Support

    Hey guys,

    I saw that theres a branch feature/swift-3 but it doesn't seem to be maintained. Are there any news regarding Swift 3 support? Would be great!

    opened by ctews 12
  • Getting EXC_BAD_INSTRUCTION sometimes while trying to fetch from cache

    Getting EXC_BAD_INSTRUCTION sometimes while trying to fetch from cache

    Sometimes the app gets an EXC_BAD_INSTRUCTION while trying to fetch a string from cache. The cache fetching is done in a NSOperation subclass (with a maxConcurrentOperation of 10). Console ouput says malloc: *** error for object 0x7fb133c5d850: double free *** set a breakpoint in malloc_error_break to debug I tried to set a breakpoint without success. Anything I did wrong?

    duplicate 
    opened by coryoso 11
  • Haneke with SwiftyJSON: 'JSON' is ambiguous for type lookup in this context

    Haneke with SwiftyJSON: 'JSON' is ambiguous for type lookup in this context

    Hello, I am working on a project that includes both SwiftyJSON and Haneke as Pods from Cocoapods. I import SwiftyJSON and HanekeSwift in my AppDelegate. However I am getting 'JSON' is ambiguous for type lookup in this context in this line statement var chatChannels: Array<JSON>?. Same thing applies if I try to only include HanekeSwift in a UITableViewCell subclass where I need to load some images from the cache.

    Any ideas?

    question 
    opened by attheodo 9
  • Updated to swift 5 and fixed all warnings.

    Updated to swift 5 and fixed all warnings.

    This PR updates HanekeSwift to swift 5.0 and Xcode 10.2.

    [Updated]

    • Updates Swift Language Version to swift 5 and Xcode 10.2.
    • Travis.yml to use Xcode 10.2
    • Podspec bump.

    [Fixed]

    • All compiler warnings.
    opened by kevnm67 8
  • May 2017, Cannot build with carthage? (or anything?)

    May 2017, Cannot build with carthage? (or anything?)

    Very sadly this excellent project seems to be dead ?

    $ cat /var/folders/xh/m9fl5v8j4hv5mpt6pc85tjz80000gn/T/carthage-xcodebuild.r92iz1.log
    /usr/bin/xcrun xcodebuild -workspace../Carthage/Checkouts/HanekeSwift/Haneke.xcworkspace -scheme Haneke -configuration Release -derivedDataPath ../Library/Caches/org.carthage.CarthageKit/DerivedData/HanekeSwift/v0.10.1 -sdk iphoneos ONLY_ACTIVE_ARCH=NO BITCODE_GENERATION_MODE=bitcode CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY= CARTHAGE=YES clean buildUser defaults from command line:
        IDEDerivedDataPathOverride = ../Library/Caches/org.carthage.CarthageKit/DerivedData/HanekeSwift/v0.10.1
    
    Build settings from command line:
        BITCODE_GENERATION_MODE = bitcode
        CARTHAGE = YES
        CODE_SIGN_IDENTITY = 
        CODE_SIGNING_REQUIRED = NO
        ONLY_ACTIVE_ARCH = NO
        SDKROOT = iphoneos10.3
    
    --- xcodebuild: WARNING: Unable to open project file ../ios/Carthage/Checkouts/HanekeSwift/Haneke.playground' in workspace '../ios/Carthage/Checkouts/HanekeSwift/Haneke.xcworkspace'.
    === CLEAN TARGET Haneke OF PROJECT Haneke WITH CONFIGURATION Release ===
    
    Check dependencies
    “Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
    “Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
    
    ** CLEAN FAILED **
    
    
    The following build commands failed:
    	Check dependencies
    (1 failure)
    === BUILD TARGET Haneke OF PROJECT Haneke WITH CONFIGURATION Release ===
    
    Check dependencies
    “Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
    “Swift Language Version” (SWIFT_VERSION) is required to be configured correctly for targets which use Swift. Use the [Edit > Convert > To Current Swift Syntax…] menu to choose a Swift version or use the Build Settings editor to configure the build setting directly.
    
    ** BUILD FAILED **
    
    
    opened by smhk 8
  • Swift 3 upgrade error...

    Swift 3 upgrade error...

    Got the following for a swift 3 migration upgrade for haneke swift... was wondering how to resolve this. Thanks!

    
    Analyzing dependencies
    Pre-downloading: `HanekeSwift` from `https://github.com/Haneke/HanekeSwift/tree/feature/swift-3`
    
    [!] Error installing HanekeSwift
    [!] /Applications/CocoaPods.app/Contents/Resources/bundle/bin/git clone https://github.com/Haneke/HanekeSwift/tree/feature/swift-3 /var/folders/3_/s_7hqrn93rz07b_4rtq0_c6r0000gp/T/d20161122-65725-c3x8io --template= --single-branch --depth 1
    
    Cloning into '/var/folders/3_/s_7hqrn93rz07b_4rtq0_c6r0000gp/T/d20161122-65725-c3x8io'...
    fatal: repository 'https://github.com/Haneke/HanekeSwift/tree/feature/swift-3/' not found
    
    
    opened by rlam3 7
  • Package.swift not included in any release

    Package.swift not included in any release

    Package.swift is checked into master but the latest (or any) release v0.11.1 does not include Package.swift so this package can't be added to a project via Swift Package Manager.

    opened by dustinburkedev 1
  • Compiling with carthage not possible with Xcode 12.0

    Compiling with carthage not possible with Xcode 12.0

    Even after carthage update, it´s still not possible to compile. Does anyone has an idea how to fix this problem? Or is it possible to make an update which will fix this issue? Thanks in advance.

    opened by Maventadorian 1
  • Troubleshooting when success is not success

    Troubleshooting when success is not success

    I never see this in my own testing but Sentry crash reports show that some real-world users are getting the following issue. I wonder how I continue to diagnose it.

    TL;DR

    Fetch (NetworkFetcher) calls the success closure but the data is apparently not ok.

               myCache.fetch(URL: url, failure: {error in
                    print("Image fetch error \(String(describing: error)) setting image \(url)")
                }, success: {data in
                    print("loaded image: \(url)")
                    if let image = UIImage(data: data) {
                        self.scrollView.display(image: image)
                    } else {
                        // This is where I want to figure out why data is not actually the image data.
                        // Would it make sense to remove this item from the cache here and have it re-download?
                    }
                })
    

    Questions / Ideas

    • Is Haneke caching the http response for 4XX or 5XX results? Then I might occasionally get a bad result from a sad server into the cache.

    • Some users could have a lot of images. Could this be a timing issue where the app gets a memory warning and the data is cleared before my code uses it to make a UIImage?

    opened by eimermusic 0
  • Create intermediate directories before writing to a file

    Create intermediate directories before writing to a file

    This pull request makes sure, that before a file is written to the disk, the folders in the path it should be written to exists. All the needed intermediate directories are created, so that the data can be written to the file without failing due to a non existing directory.

    Some already mentioned in #371 and #245, that after the removeAll() method of a cache is called, the folders are deleted, but not recreated directly. So the writes after calling removeAll() are failing with an error.

    opened by JustusvonBrandt 0
Releases(0.11.0)
  • 0.11.0(Feb 15, 2018)

  • v0.10.1(Jan 2, 2016)

    Bug fixes and improvements without API changes. Highly recommended to upgrade. Highlights:

    • #115: Bug fix: Synchronize access to UIImage initializer to workaround around Apple bug that caused rare crashes. Thanks @paulosotu!
    • 5fff29f52a0a6e290934974b8270cf45044a2bd0: [UIKit] Bug fix: Don't animate with zero duration to prevent flicker in some situations. Thanks @dstancioff!
    • #228: [NetworkFetcher] Support file:// uris by @jasoncabot.
    • e452c3cfed23896433a7f9335543565b4d3b0df0 [NetworkFetcher] Support 201 responses as suggested by @barbelith.
    • 0e79dd9b48d5477d6936b1ccb2ca6538dc52bc6f: [UIKit] Increase disk capacity for defaults formats to 50MB as suggested by @sebbean.
    • e185dd39686321aa93dc9d728b4b1d1cf7856032: [DiskFetcher] Don't fail if already cancelled.
    • #193: [DiskCache] Check if file exists before updating access time by @s0meone.
    • #253: Enable "Require Only App-Extension-Safe API" to remove warning by @poolqf.

    Last but not least a warn welcome and much appreciation to @dcharbonnier who just joined the Haneke Swift team and has already been reviewing pull requests left and right!

    Source code(tar.gz)
    Source code(zip)
Owner
Haneke
A lightweight zero-config image cache for iOS, in Swift and Objective-C.
Haneke
Cache - Nothing but Cache.

Cache doesn't claim to be unique in this area, but it's not another monster library that gives you a god's power. It does nothing but caching, but it does it well. It offers a good public API with out-of-box implementations and great customization possibilities. Cache utilizes Codable in Swift 4 to perform serialization.

HyperRedink 2.7k Dec 28, 2022
Apple Asset Cache (Content Cache) Tools

AssetCacheTool A library and tool for interacting with both the local and remote asset caches. This is based on research I did a few years ago on the

Kenneth Endfinger 21 Jan 5, 2023
MrCode is a simple GitHub iPhone App that can cache Markdown content (include images in HTML) for read it later.

MrCode is a simple GitHub iPhone App that can cache Markdown content (include images in HTML) for read it later.

hao 448 Dec 19, 2022
Cachyr A typesafe key-value data cache for iOS, macOS, tvOS and watchOS written in Swift.

Cachyr A typesafe key-value data cache for iOS, macOS, tvOS and watchOS written in Swift. There already exists plenty of cache solutions, so why creat

Norsk rikskringkasting (NRK) 124 Nov 24, 2022
Carlos - A simple but flexible cache, written in Swift for iOS 13+ and WatchOS 6 apps.

Carlos A simple but flexible cache, written in Swift for iOS 13+ and WatchOS 6 apps. Breaking Changes Carlos 1.0.0 has been migrated from PiedPiper de

National Media & Tech 628 Dec 3, 2022
CachyKit - A Caching Library is written in Swift that can cache JSON, Image, Zip or AnyObject with expiry date/TTYL and force refresh.

Nice threadsafe expirable cache management that can cache any object. Supports fetching from server, single object expire date, UIImageView loading etc.

Sadman Samee 122 Dec 28, 2022
SwiftyCache is a dynamic and auto-managed cache written in Swift

SwiftyCache is a dynamic and auto-managed cache written in Swift. Unlike a simple cache system, it allows you to keep some data even in different executions. Guaranteeing persistence, when desired, without increasing the time required to save or recover data.

Antonio Guerra 3 Aug 28, 2022
High performance cache framework for iOS.

YYCache High performance cache framework for iOS. (It's a component of YYKit) Performance You may download and compile the latest version of sqlite an

null 2.3k Dec 16, 2022
Everyone tries to implement a cache at some point in their iOS app’s lifecycle, and this is ours.

Everyone tries to implement a cache at some point in their app’s lifecycle, and this is ours. This is a library that allows people to cache NSData wit

Spotify 1.2k Dec 28, 2022
Fast, non-deadlocking parallel object cache for iOS, tvOS and OS X

PINCache Fast, non-deadlocking parallel object cache for iOS and OS X. PINCache is a fork of TMCache re-architected to fix issues with deadlocking cau

Pinterest 2.6k Dec 28, 2022
MemoryCache - type-safe, thread-safe memory cache class in Swift

MemoryCache is a memory cache class in swift. The MemoryCache class incorporates LRU policies, which ensure that a cache doesn’t

Yusuke Morishita 74 Nov 24, 2022
Track is a thread safe cache write by Swift. Composed of DiskCache and MemoryCache which support LRU.

Track is a thread safe cache write by Swift. Composed of DiskCache and MemoryCache which support LRU. Features Thread safe: Implement by dispatch_sema

Cheer 268 Nov 21, 2022
A simple cache that can hold anything, including Swift items

CacheIsKing CacheIsKing is a simple cache that allows you to store any item, including objects, pure Swift structs, enums (with associated values), et

Christopher Luu 13 Jan 22, 2018
UITableView cell cache that cures scroll-lags on cell instantiating

UITableView + Cache https://github.com/Kilograpp/UITableView-Cache UITableView cell cache that cures scroll-lags on a cell instantiating. Introduction

null 73 Aug 6, 2021
💾 Simple memory & disk cache

Cache ?? Simple memory & disk cache Usage ??‍?? Default let cache = Cache<String>() try memory.save("MyValue", forKey: "MyKey") let cached = try cac

SeongHo Hong 2 Feb 28, 2022
Cache library for videos for React Native

@lowkey/react-native-cache Cache everything Installation npm install @lowkey/react-native-cache Usage import ReactNativeCache from "@lowkey/react-nati

Max Prokopenko 1 Oct 1, 2021
CachedAsyncImage is the simplest way to add cache to your AsyncImage.

CachedAsyncImage ??️ CachedAsyncImage is AsyncImage, but with cache capabilities. Usage CachedAsyncImage has the exact same API and behavior as AsyncI

Lorenzo Fiamingo 278 Jan 5, 2023
🏈 Cache CocoaPods for faster rebuild and indexing Xcode project.

Motivation Working on a project with a huge amount of pods I had some troubles: - Slow and unnecessary indexing of pods targets, which implementation

Vyacheslav Khorkov 487 Jan 5, 2023
XCRemoteCache is a remote cache tool for Xcode projects.

XCRemoteCache is a remote cache tool for Xcode projects. It reuses target artifacts generated on a remote machine, served from a simple REST server. H

Spotify 737 Dec 27, 2022