Make large attachments fail fast

This commit is contained in:
Philipp Heckel 2022-02-12 13:51:41 -05:00
parent 9afdf5e6e7
commit 29a40080db
4 changed files with 53 additions and 25 deletions

View file

@ -1,15 +1,10 @@
package io.heckel.ntfy.msg package io.heckel.ntfy.msg
import android.net.Uri
import android.os.Build import android.os.Build
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.db.Notification import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.User import io.heckel.ntfy.db.User
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.*
import io.heckel.ntfy.util.topicUrl
import io.heckel.ntfy.util.topicUrlAuth
import io.heckel.ntfy.util.topicUrlJson
import io.heckel.ntfy.util.topicUrlJsonPoll
import okhttp3.* import okhttp3.*
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException import java.io.IOException
@ -24,12 +19,29 @@ class ApiService {
.readTimeout(15, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS) .writeTimeout(15, TimeUnit.SECONDS)
.build() .build()
private val publishClient = OkHttpClient.Builder()
.callTimeout(5, TimeUnit.MINUTES) // Total timeout for entire request
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
private val subscriberClient = OkHttpClient.Builder() private val subscriberClient = OkHttpClient.Builder()
.readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this .readTimeout(77, TimeUnit.SECONDS) // Assuming that keepalive messages are more frequent than this
.build() .build()
private val parser = NotificationParser() private val parser = NotificationParser()
fun publish(baseUrl: String, topic: String, user: User?, message: String, title: String, priority: Int, tags: List<String>, delay: String, body: RequestBody? = null, filename: String = "") { fun publish(
baseUrl: String,
topic: String,
user: User? = null,
message: String,
title: String = "",
priority: Int = 3,
tags: List<String> = emptyList(),
delay: String = "",
body: RequestBody? = null,
filename: String = ""
) {
val url = topicUrl(baseUrl, topic) val url = topicUrl(baseUrl, topic)
Log.d(TAG, "Publishing to $url") Log.d(TAG, "Publishing to $url")
@ -56,11 +68,14 @@ class ApiService {
} else { } else {
builder.put(message.toRequestBody()) builder.put(message.toRequestBody())
} }
client.newCall(builder.build()).execute().use { response -> val request = builder.build()
Log.d(TAG, request.toString())
publishClient.newCall(request).execute().use { response ->
if (response.code == 401 || response.code == 403) { if (response.code == 401 || response.code == 403) {
throw UnauthorizedException(user) throw UnauthorizedException(user)
} } else if (response.code == 413) {
if (!response.isSuccessful) { throw EntityTooLargeException()
} else if (!response.isSuccessful) {
throw Exception("Unexpected response ${response.code} when publishing to $url") throw Exception("Unexpected response ${response.code} when publishing to $url")
} }
Log.d(TAG, "Successfully published to $url") Log.d(TAG, "Successfully published to $url")
@ -149,6 +164,7 @@ class ApiService {
} }
class UnauthorizedException(val user: User?) : Exception() class UnauthorizedException(val user: User?) : Exception()
class EntityTooLargeException() : Exception()
companion object { companion object {
val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})" val USER_AGENT = "ntfy/${BuildConfig.VERSION_NAME} (${BuildConfig.FLAVOR}; Android ${Build.VERSION.RELEASE}; SDK ${Build.VERSION.SDK_INT})"

View file

@ -177,16 +177,12 @@ class ShareActivity : AppCompatActivity() {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val user = repository.getUser(baseUrl) val user = repository.getUser(baseUrl)
try { try {
val filename = if (fileUri != null) { val (filename, body) = if (fileUri != null) {
fileStat(this@ShareActivity, fileUri).filename val stat = fileStat(this@ShareActivity, fileUri)
val body = ContentUriRequestBody(applicationContext.contentResolver, fileUri!!, stat.size)
Pair(stat.filename, body)
} else { } else {
"" Pair("", null)
}
val body = if (fileUri != null) {
val resolver = applicationContext.contentResolver
ContentUriRequestBody(resolver, fileUri!!)
} else {
null
} }
api.publish( api.publish(
baseUrl = baseUrl, baseUrl = baseUrl,
@ -198,7 +194,7 @@ class ShareActivity : AppCompatActivity() {
tags = emptyList(), tags = emptyList(),
delay = "", delay = "",
body = body, // May be null body = body, // May be null
filename = filename // May be empty filename = filename, // May be empty
) )
runOnUiThread { runOnUiThread {
finish() finish()
@ -207,9 +203,20 @@ class ShareActivity : AppCompatActivity() {
.show() .show()
} }
} catch (e: Exception) { } catch (e: Exception) {
val message = if (e is ApiService.UnauthorizedException) {
if (e.user != null) {
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
} else {
getString(R.string.detail_test_message_error_unauthorized_anon)
}
} else if (e is ApiService.EntityTooLargeException) {
getString(R.string.detail_test_message_error_too_large)
} else {
getString(R.string.detail_test_message_error, e.message)
}
runOnUiThread { runOnUiThread {
progress.visibility = View.GONE progress.visibility = View.GONE
errorText.text = e.message errorText.text = message
errorImage.visibility = View.VISIBLE errorImage.visibility = View.VISIBLE
errorText.visibility = View.VISIBLE errorText.visibility = View.VISIBLE
} }

View file

@ -253,15 +253,19 @@ fun isDarkThemeOn(context: Context): Boolean {
// https://cketti.de/2020/05/23/content-uris-and-okhttp/ // https://cketti.de/2020/05/23/content-uris-and-okhttp/
class ContentUriRequestBody( class ContentUriRequestBody(
private val contentResolver: ContentResolver, private val resolver: ContentResolver,
private val contentUri: Uri private val uri: Uri,
private val size: Long
) : RequestBody() { ) : RequestBody() {
override fun contentLength(): Long {
return size
}
override fun contentType(): MediaType? { override fun contentType(): MediaType? {
val contentType = contentResolver.getType(contentUri) val contentType = resolver.getType(uri)
return contentType?.toMediaTypeOrNull() return contentType?.toMediaTypeOrNull()
} }
override fun writeTo(sink: BufferedSink) { override fun writeTo(sink: BufferedSink) {
val inputStream = contentResolver.openInputStream(contentUri) ?: throw IOException("Couldn't open content URI for reading") val inputStream = resolver.openInputStream(uri) ?: throw IOException("Couldn't open content URI for reading")
inputStream.source().use { source -> inputStream.source().use { source ->
sink.writeAll(source) sink.writeAll(source)
} }

View file

@ -129,6 +129,7 @@
<string name="detail_test_message_error">Cannot send message: %1$s</string> <string name="detail_test_message_error">Cannot send message: %1$s</string>
<string name="detail_test_message_error_unauthorized_anon">Cannot send message: Anonymous publishing not allowed</string> <string name="detail_test_message_error_unauthorized_anon">Cannot send message: Anonymous publishing not allowed</string>
<string name="detail_test_message_error_unauthorized_user">Cannot send message: User %1$s not authorized</string> <string name="detail_test_message_error_unauthorized_user">Cannot send message: User %1$s not authorized</string>
<string name="detail_test_message_error_too_large">Cannot send message: Attachment too large</string>
<string name="detail_copied_to_clipboard_message">Copied to clipboard</string> <string name="detail_copied_to_clipboard_message">Copied to clipboard</string>
<string name="detail_instant_delivery_enabled">Instant delivery enabled</string> <string name="detail_instant_delivery_enabled">Instant delivery enabled</string>
<string name="detail_instant_delivery_disabled">Instant delivery disabled</string> <string name="detail_instant_delivery_disabled">Instant delivery disabled</string>