Skip to content

Commit

Permalink
Zip backup (#933)
Browse files Browse the repository at this point in the history
* add remote zip/unzip

* reimplement: remote backup

* fix: close backup request

* get response for remote zip

* add zip drive backup

* add zip drive restore

* fix: categories are not restored

* change scope to drive.file

* add description for backup action

* refactor: Backup types
  • Loading branch information
nyagami authored Jan 31, 2024
1 parent 6c20d18 commit ad2412e
Show file tree
Hide file tree
Showing 17 changed files with 552 additions and 981 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package com.rajarsheechatterjee.ZipArchive;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

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 java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class ZipArchive extends ReactContextBaseJavaModule {
ZipArchive(ReactApplicationContext context) throws Exception {
Expand All @@ -23,36 +33,141 @@ public class ZipArchive extends ReactContextBaseJavaModule {
public String getName() {
return "ZipArchive";
}
public String escapeFilePath(String filePath){

private String escapeFilePath(String filePath){
return filePath.replaceAll(":", "\uA789");
}

private void unzipProcess(ZipInputStream zis, String distDirPath) throws Exception {
ZipEntry zipEntry;
int len;
byte[] buffer = new byte[4096];
while ((zipEntry = zis.getNextEntry()) != null) {
String escapedFilePath = this.escapeFilePath(zipEntry.getName());
File newFile = new File(distDirPath, escapedFilePath);
newFile.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(newFile);
boolean isWritten = false; // ignore folder entry;
while ((len = zis.read(buffer)) > 0) {
isWritten = true;
fos.write(buffer, 0, len);
}
fos.close();
if(!isWritten) newFile.delete();
}
zis.closeEntry();
}
@ReactMethod
public void unzip(String epubFilePath, String epubDirPath, Promise promise) {
try{
ZipInputStream zis = new ZipInputStream(new FileInputStream(epubFilePath));
ZipEntry zipEntry;
int len;
byte[] buffer = new byte[4096];
while ((zipEntry = zis.getNextEntry()) != null) {
String escapedFilePath = this.escapeFilePath(zipEntry.getName());
File newFile = new File(epubDirPath, escapedFilePath);
newFile.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(newFile);
boolean isWritten = false; // ignore folder entry;
while ((len = zis.read(buffer)) > 0) {
isWritten = true;
fos.write(buffer, 0, len);
public void unzip(String sourceFilePath, String distDirPath, Promise promise) {
new Thread(new Runnable() {
@Override
public void run() {
try{
ZipInputStream zis = new ZipInputStream(new FileInputStream(sourceFilePath));
unzipProcess(zis, distDirPath);
zis.close();
promise.resolve(null);
} catch (Exception e) {
promise.reject(e.getCause());
}
fos.close();
if(!isWritten) newFile.delete();
}
zis.closeEntry();
zis.close();
promise.resolve(null);
} catch (Exception e) {
promise.reject(e.getCause());
}
}).start();
}

@ReactMethod
public void remoteUnzip(String distDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(_url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
if(headers != null){
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
while (it.hasNext()){
Map.Entry<String, Object> entry = it.next();
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
}
}
ZipInputStream zis = new ZipInputStream(connection.getInputStream());
unzipProcess(zis, distDirPath);
connection.disconnect();
promise.resolve(null);
}catch (Exception e){
promise.reject(e.getCause());
}
}
}).start();
}

private ArrayList<String> walkDir(String path) {
ArrayList<String> res = new ArrayList<>();
File node = new File(path);
if(node.isFile()){
res.add(path);
}else{
String[] children = node.list();
if(children != null){
for(String filename: children){
ArrayList<String> childPaths = walkDir(new File(path, filename).toString());
res.addAll(childPaths);
}
}
}
return res;
}
private void zipProcess(String sourceDirPath, ZipOutputStream zos) throws Exception{
ArrayList<String> paths = walkDir(sourceDirPath);
byte[] buffer = new byte[4096];
int len;
for(String path: paths){
ZipEntry zipEntry = new ZipEntry(path.replace(sourceDirPath + "/", ""));
zos.putNextEntry(zipEntry);
try (FileInputStream fis = new FileInputStream(path)) {
while ((len = fis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
}
zos.closeEntry();
}
zos.close();
}
@ReactMethod
public void remoteZip(String sourceDirPath, String _url, @Nullable ReadableMap headers, Promise promise){
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(_url);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
if(headers != null){
Iterator<Map.Entry<String, Object>> it = headers.getEntryIterator();
while (it.hasNext()){
Map.Entry<String, Object> entry = it.next();
connection.setRequestProperty(entry.getKey(), entry.getValue().toString());
}
}
ZipOutputStream zos = new ZipOutputStream(connection.getOutputStream());
zipProcess(sourceDirPath, zos);
if(connection.getResponseCode() == 200){
InputStream is = connection.getInputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (int length; (length = is.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
}
is.close();
connection.disconnect();
promise.resolve(result.toString());
}else{
throw new Exception("Network request failed");
}
}catch (Exception e){
promise.reject(e.getCause());
}
}
}).start();
}
}
10 changes: 1 addition & 9 deletions src/api/drive/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create, download, getJson, list } from './request';
import { create, list } from './request';
import { DriveCreateRequestData, DriveFile } from './types';

const LNREADER_DRIVE_MARK = '(Do not change this!) LNReader-Drive';
Expand Down Expand Up @@ -101,11 +101,3 @@ export const readDir = async (parentId: string, marked?: boolean) => {
}
return fileList;
};

export const readFile = async (file: DriveFile, type: 'json' | 'media') => {
if (type === 'json') {
return getJson(file.id);
} else {
return download(file);
}
};
65 changes: 43 additions & 22 deletions src/api/drive/request.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { downloadFile, mkdir, exists } from 'react-native-fs';
import {
DriveCreateRequestData,
DriveFile,
DriveReponse,
DriveRequestParams,
} from './types';
import { AppDownloadFolder } from '@utils/constants/download';
import { PATH_SEPARATOR } from '@api/constants';
import ZipArchive from '@native/ZipArchive';

const BASE_URL = 'https://www.googleapis.com/drive/v3/files';
const MEDIA_UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v3/files';
Expand Down Expand Up @@ -83,35 +82,57 @@ export const create = async (
}).then(res => res.json());
};

export const getJson = async (id: string) => {
export const updateMetadata = async (
fileId: string,
fileMetaData: Partial<DriveFile>,
oldParent?: string,
) => {
const { accessToken } = await GoogleSignin.getTokens();
const url = BASE_URL + '/' + id + '?alt=media';
const url =
BASE_URL +
'/' +
fileId +
'?' +
buildParams({
addParents: fileMetaData.parents?.[0],
removeParents: oldParent,
});
return fetch(url, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
}).then(res => res.json());
body: JSON.stringify({
'name': fileMetaData.name,
'mimeType': fileMetaData.mimeType,
}),
});
};

export const uploadMedia = async (
sourceDirPath: string,
): Promise<DriveFile> => {
const { accessToken } = await GoogleSignin.getTokens();

const params: DriveRequestParams = {
fields: 'id, parents',
uploadType: 'media',
};
const url = MEDIA_UPLOAD_URL + '?' + buildParams(params);
const response = await ZipArchive.remoteZip(sourceDirPath, url, {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
});
return JSON.parse(response);
};

export const download = async (file: DriveFile) => {
export const download = async (file: DriveFile, distDirPath: string) => {
const { accessToken } = await GoogleSignin.getTokens();
const url = BASE_URL + '/' + file.id + '?alt=media';
const regex = new RegExp(`${PATH_SEPARATOR}`, 'g');
const filePath = AppDownloadFolder + '/' + file.name.replace(regex, '/');
const dirPath = filePath.split('/').slice(0, -1).join('/');
return exists(filePath).then(existed => {
if (!existed) {
return mkdir(dirPath).then(() => {
return downloadFile({
fromUrl: url,
toFile: filePath,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
});
});
}
return ZipArchive.remoteUnzip(distDirPath, url, {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
});
};
2 changes: 2 additions & 0 deletions src/api/drive/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export interface DriveRequestParams {
fields?: string;
pageToken?: string;
uploadType?: string; // only for upload
addParents?: string;
removeParents?: string;
}

export interface DriveCreateRequestData {
Expand Down
Loading

0 comments on commit ad2412e

Please sign in to comment.