DiskManager

The ZDCDiskManager simplifies the process of persisting & caching files to disk.

When you store a file to disk via the DiskManager, you can choose between two different storage modes:

Persistent Mode

Files stored in persistent mode won't be deleted unless you ask the DiskManager to delete them, or the associated node is deleted from the database.

Cache Mode

File stored in cache mode are treated as a temporarily file. They are added to a storage pool which is managed by the DiskManager. And when the max size of the storage pool is exceeded, the DiskManager automatically starts deleting files (based on when they were last accessed).

Further, cache-mode files are stored in an OS-designated Caches folder, and are available for deletion by the OS due to low-disk-space pressure.

 


API Basics

The process of storing a file in the DiskManager is called importing:

var diskImport = ZDCDiskImport(cleartextData: imageData)

try zdc.diskManager?.importNodeData(diskImport, for: imageNode)

You can import from various sources. Above, we imported an image we had in memory (possibly from the asset library). You can also import from files:

// From a normal (non-encrypted) file
var diskImport = ZDCDiskImport(cleartextFileURL: fileURL)

// Or from a cryptoFile
var diskImport = ZDCDiskImport(cryptoFile: cryptoFile)

When you import a file, you can choose whether to store the file persistently, or to store it as part of the temporary storage pool cache:

// Cache mode is the default
diskImport.storePersistently = false

// To enable persistent mode
diskImport.storePersistently = true

Importing a file is a synchronous process, so you're encouraged to do this in a background thread.

After the import function returns successfully, the file is on-disk, and being managed by the DiskManager. Further, the file is automatically encrypted.

The process of fetching a file from the DiskManager is called exporting:

let export = zdc.diskManager?.nodeData(node)
if let cryptoFile = export.cryptoFile {

  // Read cryptoFile
}

Once you have a cryptoFile, there are multiple ways in which you can read the file:

// How to read a cryptoFile:

// If it's small, you could read it into memory:
let imgData = try ZDCFileConversion.decryptCryptoFile(intoMemory: cryptoFile)

// Or you could convert it to non-encrypted file
ZDCFileConversion.decryptCryptoFile(cryptoFile, completionQueue: DispatchQueue.global())
{(fileURL: URL?, error: Error?) in
  // fileURL has non-encrypted version
}

// Or you could open it using a stream
let stream = CloudFile2CleartextInputStream(cryptoFile: cryptoFile)

// Or you could open the file for random-access
let reader = ZDCFileReader(cryptoFile: cryptoFile)

There are more options available for you in ZDCFileConversion.

 


Cache Size

You can configure the maximum size of the storage pool used to hold cached files. This can be configured per type of file:

zdc.diskManager?.maxNodeDataCacheSize = (1024 * 1024 * 50) // 50 MiB
zdc.diskManager?.maxNodeThumbnailsCacheSize = (1024 * 1024 * 10) // 10 MiB
zdc.diskManager?.maxUserAvatarsCacheSize = (1024 * 1024 * 10) // 10 MiB

Sizes are specified in bytes.

You can also query the DiskManager to determine how much space is being used:

let _ = zdc.diskManager?.storageSizeForAllNodeData()
let _ = zdc.diskManager?.storageSizeForPersistentNodeData()
let _ = zdc.diskManager?.storageSizeForCachedNodeThumbnails()
// .. and several other similarly named functions

When the size of the storage pool exceeds the configured size, the DiskManager will automatically start deleting files, starting with the least recently accessed file. One of the cool features is that the DiskManager will safely delete files for you.

 


Safe File Deletes

When you query the DiskManager, it will return an instance of ZDCCryptoFile. These cryptoFile instances contain all the information necessary to decrypt the file. (And there are many utilities built into the framework for decrypting & reading cryptoFiles.)

One of the properties of the ZDCCryptoFile is a retainToken. And this retainToken ensures that the corresponding file won't be deleted by the DiskManager so long as this retainToken is kept around in memory (i.e. not deallocated). In other words, the DiskManager will wait until all retainTokens have disappeared before deleting the file from disk.

 


Automatic migration after uploading

A common scenario is to create a new node, and store the associated file in the DiskManager. Of course, we remember that uploads aren't instantanous, and may take some time. Especially if the file is large & the user's Internet connection is slow. So we need to ensure that the file won't get deleted before it gets uploaded.

For example, in the ZeroDarkTodo sample app, we allow the user to add a photo to a Todo item. We want to store the photo in the DiskManager, but we also need to ensure it won't get deleted before the system has had a chance to upload it.

We can achieve this result by setting the storePersistently flag to true. But that's not exactly what we want. We want the file to be stored persistently, but only until the file has been uploaded. After that time, the file can be treated as a cache file. (Because, after it's in the cloud, we can always download it again as needed.)

You can achieve this result by setting 2 flags:

var diskImport = ZDCDiskImport(cleartextData: imageData)
diskImport.storePersistently = true
diskImport.migrateToCacheAfterUpload = true

try zdc.diskManager?.importNodeData(diskImport, for: imageNode)

Also available is a deleteAfterUpload option.

 


Cached File Expiration

For cached (non-persistent) files, you can optionally set an expiration interval. For example, you can specify that an imported file should be deleted after 7 days.

There are two ways to control this behavior:

Default configuration

You can set a default expiration interval for imported cache files:

zdc.diskManager?.defaultNodeThumbnailCacheExpiration = (60 * 60 * 24 * 7)

(The value you specify is in seconds.)

Per-Import configuration

You can override the default configuration per-import by explicitly setting the expiration value:

var diskImport = ZDCDiskImport(cleartextData: imageData)

// If you leave the expiration property set to zero,
// then the imported file will inherit the default value.
diskImport.expiration = 0

// If you set this value to a POSITIVE value,
// then the imported file will use this value as its expiration,
// regardless of the default settings.
diskImport.expiration = (60 * 60 * 24 * 7)

// If you set this value to a NEGATIVE value,
// then the imported file will NOT expire,
// regardless of the default settings.
diskImport.expiration = -1

Keep in mind:

  • Expiration only applies to cached files. It does not apply to files stored in persistent mode.
  • Expiration is an additional method to influence the deletion of files from the storage pool. However, even if a file doesn't have an expiration date, it's still eligible for deletion once the storage pool exceeds its max size.