Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download file with cookie persisted from RN #1100

Merged
merged 2 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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?,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions src/database/queries/ChapterQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
Expand Down Expand Up @@ -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;
}
Expand Down
36 changes: 18 additions & 18 deletions src/database/queries/NovelQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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;
};
Expand Down
22 changes: 9 additions & 13 deletions src/native/FileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ interface ReadDirResult {
}

interface FileManagerInterface {
writeFile: (
path: string,
content: string,
encoding?: 'utf8' | 'base64',
) => Promise<void>;
writeFile: (path: string, content: string) => Promise<void>;
readFile: (path: string) => Promise<string>;
resolveExternalContentUri: (uriString: string) => Promise<string | null>;
copyFile: (sourcePath: string, destPath: string) => Promise<void>;
Expand All @@ -21,17 +17,17 @@ interface FileManagerInterface {
unlink: (filePath: string) => Promise<void>; // remove recursively
readDir: (dirPath: string) => Promise<ReadDirResult[]>; // file/sub-folder names
pickFolder: () => Promise<string | null>; // return path of folderc
downloadFile: (
url: string,
destPath: string,
method: string,
headers: Record<string, string> | Headers,
body?: string,
) => Promise<void>;
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;
16 changes: 16 additions & 0 deletions src/plugins/helpers/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getUserAgent } from '@hooks/persisted/useUserAgent';
import FileManager from '@native/FileManager';
import { parse as parseProto } from 'protobufjs';

type FetchInit = {
Expand Down Expand Up @@ -80,6 +81,21 @@ export const fetchFile = async (
}
};

export const downloadFile = async (
url: string,
destPath: string,
init?: FetchInit,
): Promise<void> => {
init = makeInit(init);
return FileManager.downloadFile(
url,
destPath,
init.method || 'get',
init.headers as Record<string, string>,
init.body?.toString(),
);
};

/**
*
* @param url
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/pluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,7 +28,7 @@ const packages: Record<string, any> = {
'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 },
Expand Down
9 changes: 8 additions & 1 deletion src/plugins/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,15 @@ export interface PluginItem {
hasUpdate?: boolean;
}

export interface ImageRequestInit {
[x: string]: string | Record<string, string> | Headers | FormData | undefined;
method?: string;
headers?: Record<string, string>;
body?: string;
}

export interface Plugin extends PluginItem {
imageRequestInit?: ImageRequestInit;
filters?: Filters;
popularNovels: (
pageNo: number,
Expand All @@ -65,7 +73,6 @@ export interface Plugin extends PluginItem {
parsePage?: (novelPath: string, page: string) => Promise<SourcePage>;
parseChapter: (chapterPath: string) => Promise<string>;
searchNovels: (searchTerm: string, pageNo: number) => Promise<NovelItem[]>;
fetchImage: (url: string) => Promise<string>;
resolveUrl?: (path: string, isNovel?: boolean) => string;
webStorageUtilized?: boolean;
}
7 changes: 6 additions & 1 deletion src/screens/reader/components/WebViewReader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -150,7 +151,11 @@ const WebViewReader: FC<WebViewReaderProps> = 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}"`,
);
Expand Down
8 changes: 0 additions & 8 deletions src/services/plugin/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
Loading
Loading