Browse Source

Publish complete Android avatar bundles after crop confirmation

master
evgeny 5 days ago
parent
commit
d1080129d2
  1. 61
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt

61
tools/chatgui-android/app/src/main/java/com/utun/chat/data/AvatarRepository.kt

@ -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)

Loading…
Cancel
Save