|
| 1 | +package dev.dettmer.simplenotes.sync |
| 2 | + |
| 3 | +import com.thegrizzlylabs.sardineandroid.DavResource |
| 4 | +import com.thegrizzlylabs.sardineandroid.Sardine |
| 5 | +import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine |
| 6 | +import dev.dettmer.simplenotes.utils.Logger |
| 7 | +import okhttp3.Credentials |
| 8 | +import okhttp3.OkHttpClient |
| 9 | +import okhttp3.Request |
| 10 | +import java.io.InputStream |
| 11 | + |
| 12 | +/** |
| 13 | + * 🔧 v1.7.1: Wrapper für Sardine der Connection Leaks verhindert |
| 14 | + * |
| 15 | + * Hintergrund: |
| 16 | + * - OkHttpSardine.exists() schließt den Response-Body nicht |
| 17 | + * - Dies führt zu "connection leaked" Warnungen im Log |
| 18 | + * - Kann bei vielen Requests zu Socket-Exhaustion führen |
| 19 | + * |
| 20 | + * Lösung: |
| 21 | + * - Eigene exists()-Implementation mit korrektem Response-Cleanup |
| 22 | + * - Preemptive Authentication um 401-Round-Trips zu vermeiden |
| 23 | + * |
| 24 | + * @see <a href="https://square.github.io/okhttp/4.x/okhttp/okhttp3/-response-body/">OkHttp Response Body Docs</a> |
| 25 | + */ |
| 26 | +class SafeSardineWrapper private constructor( |
| 27 | + private val delegate: OkHttpSardine, |
| 28 | + private val okHttpClient: OkHttpClient, |
| 29 | + private val authHeader: String |
| 30 | +) : Sardine by delegate { |
| 31 | + |
| 32 | + companion object { |
| 33 | + private const val TAG = "SafeSardine" |
| 34 | + |
| 35 | + /** |
| 36 | + * Factory-Methode für SafeSardineWrapper |
| 37 | + */ |
| 38 | + fun create( |
| 39 | + okHttpClient: OkHttpClient, |
| 40 | + username: String, |
| 41 | + password: String |
| 42 | + ): SafeSardineWrapper { |
| 43 | + val delegate = OkHttpSardine(okHttpClient).apply { |
| 44 | + setCredentials(username, password) |
| 45 | + } |
| 46 | + val authHeader = Credentials.basic(username, password) |
| 47 | + return SafeSardineWrapper(delegate, okHttpClient, authHeader) |
| 48 | + } |
| 49 | + } |
| 50 | + |
| 51 | + /** |
| 52 | + * ✅ Sichere exists()-Implementation mit Response Cleanup |
| 53 | + * |
| 54 | + * Im Gegensatz zu OkHttpSardine.exists() wird hier: |
| 55 | + * 1. Preemptive Auth-Header gesendet (kein 401 Round-Trip) |
| 56 | + * 2. Response.use{} für garantiertes Cleanup verwendet |
| 57 | + */ |
| 58 | + override fun exists(url: String): Boolean { |
| 59 | + val request = Request.Builder() |
| 60 | + .url(url) |
| 61 | + .head() |
| 62 | + .header("Authorization", authHeader) |
| 63 | + .build() |
| 64 | + |
| 65 | + return try { |
| 66 | + okHttpClient.newCall(request).execute().use { response -> |
| 67 | + val isSuccess = response.isSuccessful |
| 68 | + Logger.d(TAG, "exists($url) → $isSuccess (${response.code})") |
| 69 | + isSuccess |
| 70 | + } |
| 71 | + } catch (e: Exception) { |
| 72 | + Logger.d(TAG, "exists($url) failed: ${e.message}") |
| 73 | + false |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + /** |
| 78 | + * ✅ Wrapper um get() mit Logging |
| 79 | + * |
| 80 | + * WICHTIG: Der zurückgegebene InputStream MUSS vom Caller geschlossen werden! |
| 81 | + * Empfohlen: inputStream.bufferedReader().use { it.readText() } |
| 82 | + */ |
| 83 | + override fun get(url: String): InputStream { |
| 84 | + Logger.d(TAG, "get($url)") |
| 85 | + return delegate.get(url) |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * ✅ Wrapper um list() mit Logging |
| 90 | + */ |
| 91 | + override fun list(url: String): List<DavResource> { |
| 92 | + Logger.d(TAG, "list($url)") |
| 93 | + return delegate.list(url) |
| 94 | + } |
| 95 | + |
| 96 | + /** |
| 97 | + * ✅ Wrapper um list(url, depth) mit Logging |
| 98 | + */ |
| 99 | + override fun list(url: String, depth: Int): List<DavResource> { |
| 100 | + Logger.d(TAG, "list($url, depth=$depth)") |
| 101 | + return delegate.list(url, depth) |
| 102 | + } |
| 103 | + |
| 104 | + // Alle anderen Methoden werden automatisch durch 'by delegate' weitergeleitet |
| 105 | +} |
0 commit comments