/*
* @Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved.
*/
package disklrucache
import std.io.*
import std.fs.*
import std.collection.*
import std.unicode.*
import std.posix.*
import std.convert.*
import std.regex.*
import std.sync.*
public class DiskLruCache {
static let JOURNAL_FILE: String = "journal"
static let JOURNAL_FILE_TEMP: String = "journal.tmp"
static let JOURNAL_FILE_BACKUP: String = "journal.bkp"
static let MAGIC: String = "libcore.io.DiskLruCache"
static let VERSION_1: String = "1"
static let ANY_SEQUENCE_NUMBER: Int64 = -1
static let LEGAL_KEY_PATTERN: Regex = Regex("[a-z0-9_-]{1,64}")
private static let CLEAN: String = "CLEAN"
private static let DIRTY: String = "DIRTY"
private static let REMOVE: String = "REMOVE"
private static let READ: String = "READ"
protected var directory: Path
private var journalFile: Path
private var journalFileTmp: Path
private var journalFileBackup: Path
private var appVersion: Int64
private var maxSize: Int64
protected var valueCount: Int64
private var size: Int64 = 0
private var journalWriter: Option<Sink> = Option<Sink>.None
private var lruEntries: HashMap<String, Entry> = HashMap<String, Entry>()
private var redundantOpCount: Int64 = 0
private var nextSequenceNumber: Int64 = 0
let lock: Monitor = Monitor()
private init(directory: Path, appVersion: Int64, valueCount: Int64, maxSize: Int64) {
this.directory = directory
this.appVersion = appVersion
this.journalFile = directory.join(JOURNAL_FILE)
this.journalFileTmp = directory.join(JOURNAL_FILE_TEMP)
this.journalFileBackup = directory.join(JOURNAL_FILE_BACKUP)
this.valueCount = valueCount
this.maxSize = maxSize
}
public static func open(directory: Path, appVersion: Int64, valueCount: Int64, maxSize: Int64): DiskLruCache {
if (maxSize <= 0) {
throw DiskLruCacheException("the max size of cache cannot be negative")
}
if (valueCount <= 0) {
throw DiskLruCacheException("the number of values per cache entry cannot be negative")
}
// If a bkp file exists, use it instead.
var backupFile = directory.join(JOURNAL_FILE_BACKUP)
if (exists(backupFile)) {
var journalFile = directory.join(JOURNAL_FILE)
// If journal file also exists just delete backup file.
if (exists(journalFile)) {
remove(backupFile)
} else {
renameTo(backupFile, journalFile, true)
}
}
// Prefer to pick up where we left off.
var cache: DiskLruCache = DiskLruCache(directory, appVersion, valueCount, maxSize)
if (exists(cache.journalFile)) {
try {
cache.readJournal()
cache.processJournal()
cache.journalWriter = ReallBufferSink(FileSink(File(cache.journalFile, Append)))
return cache
} catch (e: Exception) {
cache.delete()
}
}
cache = DiskLruCache(directory, appVersion, valueCount, maxSize)
cache.rebuildJournal()
return cache
}
public func delete(): Unit {
close()
Util.deleteContents(directory)
}
public func close(): Unit{
match (journalWriter) {
case None =>
return
case Some(_) =>
()
}
for (entry in lruEntries.values()) {
match (entry.currentEditor) {
case None =>
()
case Some(s) =>
s.abort()
}
}
trimToSize()
journalWriter.getOrThrow().close()
journalWriter = None
}
private func trimToSize(): Unit {
while (size > maxSize) {
let toEvict: String = lruEntries.keys().iterator().next().getOrThrow()
remove(toEvict)
}
}
public func remove(key: String): Bool {
synchronized (lock) {
checkNotClosed()
validateKey(key)
match (lruEntries.get(key)) {
case Some(k) =>
match (k.currentEditor) {
case Some(_) => return false
case None => ()
}
for (i in 0..valueCount) {
let file = k.getCleanFile(i)
file.close()
deleteIfExists(file.info.path)
size -= k.lengths[i]
k.lengths[i] = 0
}
redundantOpCount++
var jw = wirteSink(journalWriter)
jw.write(REMOVE.toArray())
jw.write(" ".toArray())
jw.write(key.toArray())
jw.write("\n".toArray())
lruEntries.remove(key)
if (journalRebuildRequired()) {
cleanUp()
}
return true
case None => return false
}
}
}
private func wirteSink(journalWriter: Option<Sink>): Sink {
return journalWriter.getOrThrow()
}
private func processJournal(): Unit {
deleteIfExists(journalFileTmp)
var str = ArrayList<String>()
for (key in lruEntries.keys()) {
let entry = lruEntries[key]
match (entry.currentEditor) {
case None =>
for (i in 0..valueCount) {
size += entry.lengths[i]
}
case Some(_) =>
entry.currentEditor = None
for (i in 0..valueCount) {
let cleanfiles = entry.getCleanFile(i)
cleanfiles.close()
let dirtyfiles = entry.getDirtyFile(i)
dirtyfiles.close()
deleteIfExists(cleanfiles.info.path)
deleteIfExists(dirtyfiles.info.path)
}
str.add(key)
}
}
lruEntries.remove(all:str)
}
private func rebuildJournal(): Unit {
synchronized (lock) {
match (journalWriter) {
case None =>
()
case Some(s) =>
s.close()
}
var filejournals: File
if (exists(journalFileTmp)) {
filejournals = File(journalFileTmp, ReadWrite)
} else {
filejournals = File(journalFileTmp, ReadWrite)
}
var writer = ReallBufferSink(FileSink(filejournals))
try {
writer.write(MAGIC.toArray())
writer.write("\n".toArray())
writer.write(VERSION_1.toArray())
writer.write("\n".toArray())
writer.write("${appVersion}".toArray())
writer.write("\n".toArray())
writer.write("${valueCount}".toArray())
writer.write("\n".toArray())
writer.write("\n".toArray())
for (entry in lruEntries.values()) {
match (entry.currentEditor) {
case Some(_) =>
writer.write((DIRTY+ " ").toArray())
writer.write(entry.key.toArray())
writer.write("\n".toArray())
case None =>
writer.write((CLEAN+ " ").toArray())
writer.write(entry.key.toArray())
writer.write(entry.getLengths().toArray())
writer.write("\n".toArray())
}
}
} finally {
writer.close()
}
if (exists(journalFile)) {
renameTo(journalFile, journalFileBackup, true);
}
renameTo(journalFileTmp, journalFile, false);
if (exists(journalFileBackup)) {
remove(journalFileBackup)
}
journalWriter = ReallBufferSink(FileSink(File(journalFile, Append)))
}
}
private func readJournal(): Unit {
var reader: StrictLineReader = StrictLineReader(File(journalFile, ReadWrite), Util.US_ASCII)
try {
let magic = reader.readLine()
let version = reader.readLine()
let appVersionString = reader.readLine()
let valueCountString = reader.readLine()
let blank = reader.readLine()
if (MAGIC != (magic) || VERSION_1 != (version) || ("${appVersion}") != (appVersionString) || ("${valueCount}") != (valueCountString) || "" != (blank)) {
throw DiskLruCacheException("unexpected journal header: [${magic}, ${version}, ${valueCountString}, ${blank}]")
}
var lineCount = 0
while (true) {
try {
readJournalLine(reader.readLine())
lineCount++
} catch (e: Exception) {
break
}
}
redundantOpCount = lineCount - lruEntries.size
} finally {
reader.close()
}
}
private func readJournalLine(line: String): Unit {
var firstSpace = -1
try {
firstSpace = line.indexOf(" ").getOrThrow()
} catch (e: Exception) {
throw DiskLruCacheException("unexpected journal line: ${line}")
}
let keyBegin = firstSpace + 1
var key: String = ""
var secondSpace = -1
var secondSpaceds: Bool = false
try {
secondSpace = line.indexOf(" ", keyBegin).getOrThrow()
} catch (e: Exception) {
secondSpaceds = true
}
if (secondSpaceds) {
key = line[keyBegin..]
if (firstSpace == REMOVE.size && line.startsWith(REMOVE)) {
lruEntries.remove(key)
return
}
} else {
key = line[keyBegin..secondSpace]
}
var entry = Entry("",this)
try {
entry = lruEntries.get(key).getOrThrow()
} catch (e: Exception) {
entry = Entry(key,this)
lruEntries.add(key, entry)
}
if (secondSpace != -1 && firstSpace == CLEAN.size && line.startsWith(CLEAN)) {
let parts: Array<String> = line[(secondSpace + 1)..].split(" ")
entry.readable = true
entry.currentEditor = None
entry.setLengths(parts)
} else if (secondSpace == -1 && firstSpace == DIRTY.size && line.startsWith(DIRTY)) {
entry.currentEditor = Editor(entry,this)
} else if (secondSpace == -1 && firstSpace == READ.size && line.startsWith(READ)) {
} else {
throw DiskLruCacheException("unexpected journal line: ${line}")
}
}
private static func deleteIfExists(file: Path): Unit {
if (exists(file)) {
try {
remove(file)
}catch (e: Exception){
throw DiskLruCacheException("failed to delete ")
}
}
}
private static func renameTo(fo: Path, to: Path, deleteDestination: Bool): Unit {
if (deleteDestination) {
deleteIfExists(to)
}
rename(fo,to:to, overwrite:true)
}
private func cleanUp(): Unit {
synchronized (lock) {
match (journalWriter) {
case Some(_) => ()
case None => return
}
trimToSize()
if (journalRebuildRequired()) {
rebuildJournal()
redundantOpCount = 0
}
}
}
private func validateKey(key: String): Unit {
let matcher: Option<MatchData> = LEGAL_KEY_PATTERN.matcher(key).fullMatch()
match (matcher) {
case Some(_) => ()
case None => throw DiskLruCacheException("keys must match regex [a-z0-9_-]{1,64}: \"" + "${key}" + "\"")
}
}
public func getDirectory(): Path {
return directory
}
public func getMaxSize(): Int64 {
return maxSize
}
public func setMaxSize(maxSize: Int64): Unit {
if (maxSize <= 0) {
throw DiskLruCacheException("invalid cache size: ${maxSize}")
}
this.maxSize = maxSize
cleanUp()
}
public func getSize(): Int64 {
return size
}
protected func completeEdit(editor: Editor, success: Bool): Unit {
synchronized (lock) {
let entry = editor.entry
if (!refEq(entry.currentEditor.getOrThrow(), editor)) { throw Exception("editor is others") }
if (success && !entry.readable) {
for (i in 0..valueCount) {
if (!editor.written.getOrThrow()[i]) {
editor.abort()
throw DiskLruCacheException("newly created entry didn't create value for index ${i}")
}
if (!exists(entry.getDirtyFile(i).info.path.toString())) {
editor.abort()
return
}
}
}
for (i in 0..valueCount) {
let dirty = entry.getDirtyFile(i)
if (success && exists(dirty.info.path.toString()) ) {
let clean = entry.getCleanFile(i)
rename(dirty.info.path,to:clean.info.path, overwrite:true)
let oldLength = entry.lengths[i]
let newLength = FileInfo(clean.info.path.toString()).size
clean.close()
entry.lengths[i] = newLength
size = size - oldLength + newLength
} else if (!success) { deleteIfExists(dirty.info.path) }
}
redundantOpCount++
entry.currentEditor = None
var jw = wirteSink(journalWriter)
if (entry.readable || success) {
entry.readable = true
jw.write((CLEAN + " ").toArray())
jw.write(entry.key.toArray())
jw.write(entry.getLengths().toArray())
jw.write("\n".toArray())
if (success) {
entry.sequenceNumber = nextSequenceNumber
nextSequenceNumber++
}
} else {
lruEntries.remove(entry.key)
jw.write((REMOVE + " ").toArray())
jw.write(entry.key.toArray())
jw.write("\n".toArray())
}
jw.flush()
if (size > maxSize || journalRebuildRequired()) { cleanUp() }
}
}
private func journalRebuildRequired(): Bool {
let redundantOpCompactThreshold = 2000
return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size
}
public func isClosed(): Bool {
synchronized (lock) {
match (journalWriter) {
case Some(_) => return false
case None => return true
}
}
}
private func checkNotClosed(): Unit {
match (journalWriter) {
case Some(_) => ()
case None =>
throw DiskLruCacheException("cache is closed")
}
}
public func flush(): Unit {
synchronized (lock) {
checkNotClosed()
trimToSize()
journalWriter.getOrThrow().flush()
}
}
protected static func inputStreamToString(ins: InputStream): String {
return Util.readFully(StringReader(ins))
}
public func edit(key: String): Option<Editor> {
return edit(key, ANY_SEQUENCE_NUMBER)
}
protected func edit(key: String, expectedSequenceNumber: Int64): Option<Editor> {
synchronized (lock) {
checkNotClosed()
validateKey(key)
var entrys: Option<Entry> = lruEntries.get(key)
var entry = Entry(key, this)
var hasNull: Bool = false
match (entrys) {
case Some(v) =>
entry = v
case None =>
hasNull = true
}
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (hasNull || entry.sequenceNumber != expectedSequenceNumber)) {
return None
}
if (hasNull) {
entry = Entry(key, this)
lruEntries.add(key, entry)
}
match (entry.currentEditor) {
case Some(_) =>
return None
case None =>
()
}
var editor = Editor(entry,this)
entry.currentEditor = Option<Editor>.Some(editor)
let jw = journalWriter.getOrThrow()
jw.write(DIRTY.toArray())
jw.write(" ".toArray())
jw.write(key.toArray())
jw.write("\n".toArray())
jw.flush()
return Option<Editor>.Some(editor)
}
}
public func get(key: String): Option<Snapshot> {
synchronized (lock) {
checkNotClosed()
validateKey(key)
var entryso: Option<Entry> = lruEntries.get(key)
var entry = Entry(key, this)
match (entryso) {
case Some(v) =>
entry = v
case None =>
return None
}
if (!entry.readable) {
return None
}
var ins: Array<File> = Array<File>(valueCount, repeat: unsafe{zeroValue<File>()})
for (i in 0..valueCount) {
ins[i] = entry.getCleanFile(i)
}
redundantOpCount++
let jw = journalWriter.getOrThrow()
jw.write(READ.toArray())
jw.write(" ".toArray())
jw.write(key.toArray())
jw.write("\n".toArray())
if (journalRebuildRequired()) {
cleanUp()
}
return Snapshot(key, entry.sequenceNumber, ins, entry.lengths, this)
}
}
}
class Entry {
protected var key: String
protected var lengths: Array<Int64>
protected var readable: Bool = false
protected var currentEditor: Option<Editor> = None
protected var sequenceNumber: Int64 = 0
private var disk: DiskLruCache
init(key: String, disk: DiskLruCache) {
this.disk = disk
this.key = key
this.lengths = Array<Int64>(disk.valueCount, repeat: 0)
}
public func getLengths(): String {
var result = StringBuilder()
for (src in lengths) {
result.append(' ')
result.append(src)
}
return result.toString()
}
public func setLengths(strings: Array<String>): Unit {
if (strings.size != disk.valueCount) {
throw DiskLruCacheException("unexpected journal line: ${strings.toString()}" )
}
try {
for (i in 0..strings.size) {
lengths[i] = Int64.parse(strings[i])
}
} catch (e: Exception) {
throw DiskLruCacheException("unexpected journal line: ${strings.toString()}" )
}
}
public func getCleanFile(i: Int64): File {
if(exists(disk.directory.join(key+"."+"${i}"))) {
return File(disk.directory.join(key+"."+"${i}"), ReadWrite)
} else {
return File(disk.directory.join(key+"."+"${i}"), ReadWrite)
}
}
public func getDirtyFile(i: Int64): File {
if(exists(disk.directory.join(key+"."+"${i}"+".tmp"))) {
return File(disk.directory.join(key+"."+"${i}"+".tmp"), ReadWrite)
} else {
return File(disk.directory.join(key+"."+"${i}"+".tmp"), ReadWrite)
}
}
}
public class Editor {
var entry: Entry
protected let written: Option<Array<Bool>>
private var hasErrors: Bool = false
private var committed: Bool = false
private let disk: DiskLruCache
private let lock: Monitor = Monitor()
init(entry: Entry, disk: DiskLruCache) {
this.entry = entry
this.disk = disk
this.written = if (!entry.readable) {
Array<Bool>(disk.valueCount, repeat: false)
} else {
Option<Array<Bool>>.None
}
}
public func abort(): Unit {
disk.completeEdit(this, false)
}
public func newInputStream(index: Int64): Option<InputStream> {
synchronized (disk.lock) {
if(!refEq(entry.currentEditor.getOrThrow(), this)) {
throw DiskLruCacheException("editor object inconsistency")
}
if (!entry.readable) {
return Option<InputStream>.None
}
try {
return Option<InputStream>.Some(entry.getCleanFile(index))
} catch (e: Exception) {
return Option<InputStream>.None
}
}
}
public func getString(index: Int64): String {
let inse: Option<InputStream> = newInputStream(index)
match (inse) {
case Some(K) =>
return DiskLruCache.inputStreamToString(K)
case None =>
return ""
}
}
public func newOutputStream(index: Int64): File {
synchronized(disk.lock) {
if(!refEq(entry.currentEditor.getOrThrow(), this)) {
throw DiskLruCacheException("editor object inconsistency")
}
if (!entry.readable) {
let writtense = written.getOrThrow()
writtense[index] = true
}
let dirtyFile: File = entry.getDirtyFile(index)
return dirtyFile
}
}
public func set(index: Int64, value: String): Unit {
let writer = newOutputStream(index)
try {
if (writer.canWrite()) {
writer.write(value.toArray())
}
} finally {
writer.close()
}
}
public func commit(): Unit {
if (hasErrors) {
disk.completeEdit(this, false)
disk.remove(entry.key)
} else {
disk.completeEdit(this, true)
}
committed = true
}
public func abortUnlessCommitted(): Unit {
if (!committed) {
try {
abort()
} catch (ignored: Exception) {
}
}
}
}
public class Snapshot {
private var buff = StringBuilder()
private var buffArr = Array<Byte>(8192, repeat: 0)
private var key: String
private var sequenceNumber: Int64
private var ins: Array<File>
private var lengths: Array<Int64>
private var disk: DiskLruCache
private var isClosed: Bool = false
init(key: String, sequenceNumber: Int64, ins: Array<File>, lengths: Array<Int64>, disk: DiskLruCache) {
this.key = key
this.sequenceNumber = sequenceNumber
this.ins = ins
this.lengths = lengths
this.disk = disk
}
public func edit(): Option<Editor> {
return disk.edit(key, sequenceNumber)
}
public func getInputStream(index: Int64): File {
if (isClosed) {
throw DiskLruCacheException("stream closed")
}
let src = ins.get(index).getOrThrow()
return src
}
public func getString(index: Int64): String {
buff.reset()
let file = getInputStream(index)
var read = file.read(buffArr)
while (read != 0) {
unsafe {buff.appendFromUtf8Unchecked(buffArr.slice(0, read))}
read = file.read(buffArr)
}
return buff.toString()
}
public func getLength(index: Int64): Int64 {
return lengths[index]
}
public func close(): Unit {
isClosed = true
for (fse in ins) {
Util.closeQuietly(fse)
}
}
}
public class DiskLruCacheException <: Exception {
private var messages: String = ""
public init() {
super()
}
public init(messages: String) {
super(messages)
this.messages = messages
}
public func getMessage(): String {
return messages
}
public override func toString(): String {
return "DiskLruCacheException: ${messages}"
}
}