From 054c332837ed21b32b1363e252bfdcdd97f669a9 Mon Sep 17 00:00:00 2001 From: nyagami Date: Sat, 8 Jun 2024 18:53:01 +0700 Subject: [PATCH 1/2] download file --- .../FileManager/FileManager.kt | 90 +++++++++++++++---- src/database/queries/ChapterQueries.ts | 6 +- src/database/queries/NovelQueries.ts | 36 ++++---- src/native/FileManager.ts | 22 ++--- src/plugins/helpers/fetch.ts | 16 ++++ src/plugins/pluginManager.ts | 4 +- src/plugins/types/index.ts | 9 +- .../reader/components/WebViewReader.tsx | 10 ++- src/services/plugin/fetch.ts | 8 -- src/services/updates/LibraryUpdateQueries.ts | 23 +++-- 10 files changed, 149 insertions(+), 75 deletions(-) diff --git a/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt b/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt index b24c32e77..a4af0262a 100644 --- a/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt +++ b/android/app/src/main/java/com/rajarsheechatterjee/FileManager/FileManager.kt @@ -6,22 +6,36 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.DocumentsContract -import android.util.Base64 import com.facebook.react.bridge.BaseActivityEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.bridge.WritableNativeMap -import kotlinx.coroutines.MainScope +import com.facebook.react.modules.network.CookieJarContainer +import com.facebook.react.modules.network.ForwardingCookieHandler +import com.facebook.react.modules.network.OkHttpClientProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Headers +import okhttp3.JavaNetCookieJar +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okio.buffer +import okio.sink import java.io.BufferedReader import java.io.File import java.io.FileReader import java.io.FileWriter +import java.io.IOException import java.io.InputStream import java.io.OutputStream @@ -31,8 +45,9 @@ class FileManager(context: ReactApplicationContext) : return "FileManager" } + private val okHttpClient = OkHttpClientProvider.createClient() private var _promise: Promise? = null - private val coroutineScope = MainScope() + private val coroutineScope = CoroutineScope(Dispatchers.IO) private val activityEventListener = object : BaseActivityEventListener() { override fun onActivityResult( activity: Activity?, @@ -60,6 +75,9 @@ class FileManager(context: ReactApplicationContext) : init { context.addActivityEventListener(activityEventListener) + val cookieContainer = okHttpClient.cookieJar as CookieJarContainer + val cookieHandler = ForwardingCookieHandler(reactApplicationContext) + cookieContainer.setCookieJar(JavaNetCookieJar(cookieHandler)) } private fun getFileUri(filepath: String): Uri { @@ -91,18 +109,11 @@ class FileManager(context: ReactApplicationContext) : } @ReactMethod - fun writeFile(path: String, content: String, encoding: String?, promise: Promise) { + fun writeFile(path: String, content: String, promise: Promise) { try { - if (encoding == null || encoding == "utf8") { - val fw = FileWriter(path) - fw.write(content) - fw.close() - } else { - val bytes = Base64.decode(content, Base64.DEFAULT) - val os = getOutputStream(path) - os.write(bytes) - os.close() - } + val fw = FileWriter(path) + fw.write(content) + fw.close() promise.resolve(null) } catch (e: Exception) { promise.reject(e) @@ -147,7 +158,7 @@ class FileManager(context: ReactApplicationContext) : onDone: (() -> Unit)? = null, promise: Promise? = null ) { - coroutineScope.launch { + coroutineScope.launch(Dispatchers.IO) { val inputStream = getInputStream(filepath) val outputStream = getOutputStream(destPath) val buffer = ByteArray(1024) @@ -179,7 +190,7 @@ class FileManager(context: ReactApplicationContext) : fun mkdir(filepath: String, promise: Promise) { try { val file = File(filepath) - if(!file.exists()){ + if (!file.exists()) { val created = file.mkdirs() if (!created) throw Exception("Directory could not be created") } @@ -271,6 +282,53 @@ class FileManager(context: ReactApplicationContext) : currentActivity?.startActivityForResult(intent, FOLDER_PICKER_REQUEST) } + @ReactMethod + fun downloadFile( + url: String, + destPath: String, + method: String, + headers: ReadableMap, + body: String?, + promise: Promise + ) { + coroutineScope.launch { + try { + val headersBuilder = Headers.Builder() + headers.entryIterator.forEach { entry -> + headersBuilder.add(entry.key, entry.value.toString()) + } + val requestBuilder = Request.Builder() + .url(url) + .headers(headersBuilder.build()) + if (method.lowercase() == "get") { + requestBuilder.get() + } else if (body != null) { + requestBuilder.post(body.toRequestBody()) + } + + okHttpClient.newCall(requestBuilder.build()) + .enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + promise.reject(e) + } + + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful || response.body == null) { + promise.reject(Exception("Failed to download load: ${response.code}")) + return + } + val sink = File(destPath).sink().buffer() + response.body!!.source().readAll(sink) + sink.close() + promise.resolve(null) + } + }) + } catch (e: Exception) { + promise.reject(e) + } + } + } + companion object { const val FOLDER_PICKER_REQUEST = 1 } diff --git a/src/database/queries/ChapterQueries.ts b/src/database/queries/ChapterQueries.ts index 2c701755e..0296c9d29 100644 --- a/src/database/queries/ChapterQueries.ts +++ b/src/database/queries/ChapterQueries.ts @@ -12,6 +12,7 @@ import { noop } from 'lodash-es'; import { getString } from '@strings/translations'; import FileManager from '@native/FileManager'; import { NOVEL_STORAGE } from '@utils/Storages'; +import { downloadFile } from '@plugins/helpers/fetch'; const db = SQLite.openDatabase('lnreader.db'); const insertChapterQuery = ` @@ -260,13 +261,12 @@ const downloadFiles = async ( const elem = loadedCheerio(imgs[i]); const url = elem.attr('src'); if (url) { - const imageb64 = await plugin.fetchImage(url); const fileurl = folder + i + '.b64.png'; elem.attr('src', `file://${fileurl}`); - FileManager.writeFile(fileurl, imageb64, 'base64'); + await downloadFile(url, fileurl, plugin.imageRequestInit); } } - FileManager.writeFile(folder + 'index.html', loadedCheerio.html()); + await FileManager.writeFile(folder + 'index.html', loadedCheerio.html()); } catch (error) { throw error; } diff --git a/src/database/queries/NovelQueries.ts b/src/database/queries/NovelQueries.ts index 00dbda8cd..986167c9b 100644 --- a/src/database/queries/NovelQueries.ts +++ b/src/database/queries/NovelQueries.ts @@ -3,7 +3,7 @@ const db = SQLite.openDatabase('lnreader.db'); import * as DocumentPicker from 'expo-document-picker'; -import { fetchImage, fetchNovel } from '@services/plugin/fetch'; +import { fetchNovel } from '@services/plugin/fetch'; import { insertChapters } from './ChapterQueries'; import { showToast } from '@utils/showToast'; @@ -14,6 +14,8 @@ import { BackupNovel, NovelInfo } from '../types'; import { SourceNovel } from '@plugins/types'; import { NOVEL_STORAGE } from '@utils/Storages'; import FileManager from '@native/FileManager'; +import { downloadFile } from '@plugins/helpers/fetch'; +import { getPlugin } from '@plugins/pluginManager'; export const insertNovelAndChapters = async ( pluginId: string, @@ -43,27 +45,25 @@ export const insertNovelAndChapters = async ( }); }); if (novelId) { - const promises = [insertChapters(novelId, sourceNovel.chapters)]; if (sourceNovel.cover) { const novelDir = NOVEL_STORAGE + '/' + pluginId + '/' + novelId; await FileManager.mkdir(novelDir); - const novelCoverUri = 'file://' + novelDir + '/cover.png'; - promises.push( - fetchImage(pluginId, sourceNovel.cover).then(base64 => { - if (base64) { - FileManager.writeFile(novelCoverUri, base64, 'base64').then(() => { - db.transaction(tx => { - tx.executeSql('UPDATE Novel SET cover = ? WHERE id = ?', [ - novelCoverUri, - novelId, - ]); - }); - }); - } - }), - ); + const novelCoverPath = novelDir + '/cover.png'; + const novelCoverUri = 'file://' + novelCoverPath; + downloadFile( + sourceNovel.cover, + novelCoverPath, + getPlugin(pluginId)?.imageRequestInit, + ).then(() => { + db.transaction(tx => { + tx.executeSql('UPDATE Novel SET cover = ? WHERE id = ?', [ + novelCoverUri, + novelId, + ]); + }); + }); } - await Promise.all(promises); + await insertChapters(novelId, sourceNovel.chapters); } return novelId; }; diff --git a/src/native/FileManager.ts b/src/native/FileManager.ts index 01dbeed9d..262b1de17 100644 --- a/src/native/FileManager.ts +++ b/src/native/FileManager.ts @@ -7,11 +7,7 @@ interface ReadDirResult { } interface FileManagerInterface { - writeFile: ( - path: string, - content: string, - encoding?: 'utf8' | 'base64', - ) => Promise; + writeFile: (path: string, content: string) => Promise; readFile: (path: string) => Promise; resolveExternalContentUri: (uriString: string) => Promise; copyFile: (sourcePath: string, destPath: string) => Promise; @@ -21,17 +17,17 @@ interface FileManagerInterface { unlink: (filePath: string) => Promise; // remove recursively readDir: (dirPath: string) => Promise; // file/sub-folder names pickFolder: () => Promise; // return path of folderc + downloadFile: ( + url: string, + destPath: string, + method: string, + headers: Record | Headers, + body?: string, + ) => Promise; ExternalDirectoryPath: string; ExternalCachesDirectoryPath: string; } const { FileManager } = NativeModules; -const _FileManager = { - ...FileManager, - writeFile: (path: string, destPath: string, encoding?: 'utf8' | 'base64') => { - return FileManager.writeFile(path, destPath, encoding || null); - }, -}; - -export default _FileManager as FileManagerInterface; +export default FileManager as FileManagerInterface; diff --git a/src/plugins/helpers/fetch.ts b/src/plugins/helpers/fetch.ts index fd23491c9..89f896921 100644 --- a/src/plugins/helpers/fetch.ts +++ b/src/plugins/helpers/fetch.ts @@ -1,4 +1,5 @@ import { getUserAgent } from '@hooks/persisted/useUserAgent'; +import FileManager from '@native/FileManager'; import { parse as parseProto } from 'protobufjs'; type FetchInit = { @@ -80,6 +81,21 @@ export const fetchFile = async ( } }; +export const downloadFile = async ( + url: string, + destPath: string, + init?: FetchInit, +): Promise => { + init = makeInit(init); + return FileManager.downloadFile( + url, + destPath, + init.method || 'get', + init.headers as Record, + init.body?.toString(), + ); +}; + /** * * @param url diff --git a/src/plugins/pluginManager.ts b/src/plugins/pluginManager.ts index a51fd68c2..d01fded8b 100644 --- a/src/plugins/pluginManager.ts +++ b/src/plugins/pluginManager.ts @@ -9,7 +9,7 @@ import qs from 'qs'; import { NovelStatus, Plugin, PluginItem } from './types'; import { FilterTypes } from './types/filterTypes'; import { isUrlAbsolute } from './helpers/isAbsoluteUrl'; -import { fetchApi, fetchFile, fetchProto, fetchText } from './helpers/fetch'; +import { fetchApi, fetchProto, fetchText } from './helpers/fetch'; import { defaultCover } from './helpers/constants'; import { Storage, LocalStorage, SessionStorage } from './helpers/storage'; import { encode, decode } from 'urlencode'; @@ -28,7 +28,7 @@ const packages: Record = { 'qs': qs, 'urlencode': { encode, decode }, '@libs/novelStatus': { NovelStatus }, - '@libs/fetch': { fetchApi, fetchFile, fetchText, fetchProto }, + '@libs/fetch': { fetchApi, fetchText, fetchProto }, '@libs/isAbsoluteUrl': { isUrlAbsolute }, '@libs/filterInputs': { FilterTypes }, '@libs/defaultCover': { defaultCover }, diff --git a/src/plugins/types/index.ts b/src/plugins/types/index.ts index 64777b63c..8c0119259 100644 --- a/src/plugins/types/index.ts +++ b/src/plugins/types/index.ts @@ -55,7 +55,15 @@ export interface PluginItem { hasUpdate?: boolean; } +export interface ImageRequestInit { + [x: string]: string | Record | Headers | FormData | undefined; + method?: string; + headers?: Record; + body?: string; +} + export interface Plugin extends PluginItem { + imageRequestInit?: ImageRequestInit; filters?: Filters; popularNovels: ( pageNo: number, @@ -65,7 +73,6 @@ export interface Plugin extends PluginItem { parsePage?: (novelPath: string, page: string) => Promise; parseChapter: (chapterPath: string) => Promise; searchNovels: (searchTerm: string, pageNo: number) => Promise; - fetchImage: (url: string) => Promise; resolveUrl?: (path: string, isNovel?: boolean) => string; webStorageUtilized?: boolean; } diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index c6ee4746d..bbb1ec0b3 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -26,6 +26,7 @@ import { getBatteryLevelSync } from 'react-native-device-info'; import * as Speech from 'expo-speech'; import * as Clipboard from 'expo-clipboard'; import { showToast } from '@utils/showToast'; +import { fetchFile } from '@plugins/helpers/fetch'; type WebViewPostEvent = { type: string; @@ -62,7 +63,8 @@ const WebViewReader: FC = props => { onWebViewNavigationStateChange, } = props; const assetsUriPrefix = useMemo( - () => (__DEV__ ? 'http://localhost:8081/assets' : 'file:///android_asset'), + () => + __DEV__ ? 'http://192.168.98.106:8081/assets' : 'file:///android_asset', [], ); const { RNDeviceInfo } = NativeModules; @@ -150,7 +152,11 @@ const WebViewReader: FC = props => { break; case 'error-img': if (event.data && typeof event.data === 'string') { - plugin?.fetchImage(event.data).then(base64 => { + fetchFile(event.data, { + method: plugin?.imageRequestInit?.method, + headers: plugin?.imageRequestInit?.headers, + body: plugin?.imageRequestInit?.body, + }).then(base64 => { webViewRef.current?.injectJavaScript( `document.querySelector("img[error-src='${event.data}']").src="data:image/jpg;base64,${base64}"`, ); diff --git a/src/services/plugin/fetch.ts b/src/services/plugin/fetch.ts index 1cbc1eac2..c991b450d 100644 --- a/src/services/plugin/fetch.ts +++ b/src/services/plugin/fetch.ts @@ -10,14 +10,6 @@ export const fetchNovel = async (pluginId: string, novelPath: string) => { return res; }; -export const fetchImage = async (pluginId: string, imageUrl: string) => { - const plugin = getPlugin(pluginId); - if (!plugin) { - throw new Error(`Unknown plugin: ${pluginId}`); - } - return plugin.fetchImage(imageUrl); -}; - export const fetchChapter = async (pluginId: string, chapterPath: string) => { const plugin = getPlugin(pluginId); let chapterText = `Unkown plugin: ${pluginId}`; diff --git a/src/services/updates/LibraryUpdateQueries.ts b/src/services/updates/LibraryUpdateQueries.ts index e4a3d138b..14f9ae50b 100644 --- a/src/services/updates/LibraryUpdateQueries.ts +++ b/src/services/updates/LibraryUpdateQueries.ts @@ -1,11 +1,12 @@ -import { fetchImage, fetchNovel, fetchPage } from '../plugin/fetch'; +import { fetchNovel, fetchPage } from '../plugin/fetch'; import { downloadChapter } from '../../database/queries/ChapterQueries'; import * as SQLite from 'expo-sqlite'; import { ChapterItem, SourceNovel } from '@plugins/types'; -import { LOCAL_PLUGIN_ID } from '@plugins/pluginManager'; +import { getPlugin, LOCAL_PLUGIN_ID } from '@plugins/pluginManager'; import FileManager from '@native/FileManager'; import { NOVEL_STORAGE } from '@utils/Storages'; +import { downloadFile } from '@plugins/helpers/fetch'; const db = SQLite.openDatabase('lnreader.db'); const updateNovelMetadata = ( @@ -21,16 +22,14 @@ const updateNovelMetadata = ( await FileManager.mkdir(novelDir); } if (cover) { - const novelCoverUri = 'file://' + novelDir + '/cover.png'; - await fetchImage(pluginId, cover) - .then(base64 => { - if (base64) { - cover = novelCoverUri; - return FileManager.writeFile(novelCoverUri, base64, 'base64'); - } - }) - .catch(reject); - cover += '?' + Date.now(); + const novelCoverPath = novelDir + '/cover.png'; + const novelCoverUri = 'file://' + novelCoverPath; + await downloadFile( + cover, + novelCoverPath, + getPlugin(pluginId)?.imageRequestInit, + ); + cover = novelCoverUri + '?' + Date.now(); } db.transaction(tx => { tx.executeSql( From c7787be144d43b28d0e5dce1bafae38b2774944a Mon Sep 17 00:00:00 2001 From: nyagami Date: Sat, 8 Jun 2024 20:13:30 +0700 Subject: [PATCH 2/2] clean up --- src/screens/reader/components/WebViewReader.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index bbb1ec0b3..1bef2c5b5 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -63,8 +63,7 @@ const WebViewReader: FC = props => { onWebViewNavigationStateChange, } = props; const assetsUriPrefix = useMemo( - () => - __DEV__ ? 'http://192.168.98.106:8081/assets' : 'file:///android_asset', + () => (__DEV__ ? 'http://localhost:8081/assets' : 'file:///android_asset'), [], ); const { RNDeviceInfo } = NativeModules;