|
|
|
|
@ -400,12 +400,17 @@ class AvatarRepository(context: Context) {
|
|
|
|
|
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: throw IOException("image decode failed") |
|
|
|
|
} |
|
|
|
|
private fun prune(root: File) { |
|
|
|
|
val pinned = defaultAvatar?.substringAfter('/') |
|
|
|
|
val pinned=mutableSetOf<String>() |
|
|
|
|
defaultAvatar?.substringAfter('/')?.takeIf { AvatarProtocol.validFile(it) }?.let { name -> |
|
|
|
|
pinned+=name |
|
|
|
|
try { val r=AvatarAssets.record(root,name); pinned+=r.original; pinned+=r.preview } |
|
|
|
|
catch (e: Exception) { log("WARN", "Cannot pin profile bundle=$name: ${e.message}") } |
|
|
|
|
} |
|
|
|
|
val entries = root.listFiles() |
|
|
|
|
if (entries == null) { log("WARN", "Cannot enumerate avatar cache directory=$root"); return } |
|
|
|
|
val files = entries.filter { AvatarProtocol.validFile(it.name) }.sortedBy { it.lastModified() } |
|
|
|
|
val files = entries.filter { AvatarProtocol.validObject(it.name) }.sortedBy { it.lastModified() } |
|
|
|
|
var size = files.sumOf { it.length() } |
|
|
|
|
for (file in files) if (size > 100L * 1024 * 1024 && file.name != pinned) { |
|
|
|
|
for (file in files) if (size > 100L * 1024 * 1024 && file.name !in pinned) { |
|
|
|
|
val bytes = file.length() |
|
|
|
|
if (file.delete()) size -= bytes else log("WARN", "Cache eviction failed file=$file") |
|
|
|
|
} |
|
|
|
|
@ -420,25 +425,20 @@ class AvatarRepository(context: Context) {
|
|
|
|
|
ensureGroup(group, true) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun editProfile(server: Long, uri: Uri?, expectedAccount: Long) = edit("profile", expectedAccount) { epoch -> |
|
|
|
|
fun editProfile(server: Long, source: File?, expectedAccount: Long, crop: AvatarCrop? = null) = edit("profile", expectedAccount) { epoch -> |
|
|
|
|
val uid = account |
|
|
|
|
val bytes = if (uri == null) null else prepare(uri, false) |
|
|
|
|
val prepared=if (source==null) null else try { |
|
|
|
|
withContext(Dispatchers.IO) { AvatarAssets.prepare(source,directory(uid),requireNotNull(crop)) } |
|
|
|
|
} finally { withContext(NonCancellable+Dispatchers.IO) { |
|
|
|
|
if (!source.delete()) log("WARN", "Cannot remove editor source=$source") |
|
|
|
|
} } |
|
|
|
|
alive(epoch) |
|
|
|
|
if (bytes != null) { |
|
|
|
|
if (prepared != null) { |
|
|
|
|
require(server != 0L && storageServers().any { it.nodeId == server }) { "no eligible storage server" } |
|
|
|
|
val file = "${digest(bytes)}.png" |
|
|
|
|
withContext(Dispatchers.IO) { |
|
|
|
|
val root = directory(uid) |
|
|
|
|
if (!root.isDirectory && !root.mkdirs()) throw IOException("cannot create profile cache") |
|
|
|
|
val temp = File(root, ".profile-${ids.incrementAndGet()}") |
|
|
|
|
try { |
|
|
|
|
temp.writeBytes(bytes) |
|
|
|
|
if (!temp.renameTo(File(root, file))) throw IOException("cannot publish profile cache") |
|
|
|
|
} finally { if (temp.exists() && !temp.delete()) log("WARN", "Cannot remove profile temp=$temp") } |
|
|
|
|
} |
|
|
|
|
val (file,bitmap)=prepared |
|
|
|
|
alive(epoch) |
|
|
|
|
defaultAvatar = "${AvatarProtocol.hex(server)}/$file" |
|
|
|
|
cache.put(file, withContext(Dispatchers.IO) { decode(bytes, file.removeSuffix(".png"), false) }) |
|
|
|
|
cache.put(file,bitmap) |
|
|
|
|
} else defaultAvatar = "-" |
|
|
|
|
alive(epoch) |
|
|
|
|
val desired = defaultAvatar |
|
|
|
|
@ -486,18 +486,25 @@ class AvatarRepository(context: Context) {
|
|
|
|
|
foreground.first { it } |
|
|
|
|
transfers.withPermit { |
|
|
|
|
if (server != 0L) { |
|
|
|
|
val local = File(directory(uid), file) |
|
|
|
|
withContext(Dispatchers.IO) { decode(readImage(local), file.removeSuffix(".png"), false) } |
|
|
|
|
val root=directory(uid) |
|
|
|
|
val record=withContext(Dispatchers.IO) { AvatarAssets.record(root,file) } |
|
|
|
|
val mkdir = AvatarProtocol.response(request(49, group, server, uid, 8) { id, gid -> NativeLib.avatarMkdir(id, gid, server, uid) }) |
|
|
|
|
require(mkdir.status == 0 || mkdir.status == 5) { "mkdir status=${mkdir.status}" } |
|
|
|
|
val stat = AvatarProtocol.response(request(49, group, server, uid, 2) { id, gid -> NativeLib.avatarStat(id, gid, server, uid, "avatars/$file") }) |
|
|
|
|
if (stat.status == 2) { |
|
|
|
|
val upload = AvatarProtocol.transfer(request(50, group, server, uid) { id, gid -> |
|
|
|
|
NativeLib.transferAvatar(id, gid, server, uid, "avatars/$file", local.path, true) |
|
|
|
|
}) ?: throw IOException("missing upload result") |
|
|
|
|
require(upload.status == 0) { "upload status=${upload.status}" } |
|
|
|
|
} else require(stat.status == 0 && stat.fileType == 1 && stat.size == local.length()) { |
|
|
|
|
"stat status=${stat.status} type=${stat.fileType} bytes=${stat.size} expected=${local.length()}" |
|
|
|
|
for (name in listOf(record.original,record.preview,file)) { |
|
|
|
|
val local=File(root,name) |
|
|
|
|
val limit=if (name.endsWith(".json")) AvatarProtocol.MAX_MANIFEST else AvatarProtocol.MAX_ORIGINAL |
|
|
|
|
withContext(Dispatchers.IO) { AvatarAssets.read(local,name,limit) } |
|
|
|
|
val stat=AvatarProtocol.response(request(49,group,server,uid,2) { id,gid -> |
|
|
|
|
NativeLib.avatarStat(id,gid,server,uid,"avatars/$name") |
|
|
|
|
}) |
|
|
|
|
if (stat.status==2) { |
|
|
|
|
val upload=AvatarProtocol.transfer(request(50,group,server,uid) { id,gid -> |
|
|
|
|
NativeLib.transferAvatar(id,gid,server,uid,"avatars/$name",local.path,true,limit) |
|
|
|
|
}) ?: throw IOException("missing upload result") |
|
|
|
|
require(upload.status==0) { "upload file=$name status=${upload.status}" } |
|
|
|
|
} else require(stat.status==0 && stat.fileType==1 && stat.size==local.length()) { |
|
|
|
|
"stat file=$name status=${stat.status} bytes=${stat.size} expected=${local.length()}" |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
alive(epoch) |
|
|
|
|
|