Skip to content

Commit 3b7a529

Browse files
committed
feat: add option to zip files before sending by email
1 parent fec8d38 commit 3b7a529

File tree

8 files changed

+123
-40
lines changed

8 files changed

+123
-40
lines changed

README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ Initialize the file-logger with the specified options. As soon as the returned p
5050

5151
Send all log files by email. On iOS, it uses `MFMailComposeViewController` to ensure that the user won't leave the app when sending log files.
5252

53-
| Option | Description |
54-
| --------- | ------------------------------------ |
55-
| `to` | Email address of the recipient |
56-
| `subject` | Email subject |
57-
| `body` | Plain text body message of the email |
53+
| Option | Description |
54+
| ---------- | --------------------------------------------------------------------------------------------------------------------- |
55+
| `to` | Email address of the recipient |
56+
| `subject` | Email subject |
57+
| `body` | Plain text body message of the email |
58+
| `compress` | If `true`, log files will be compressed into a single zip file before being attached to the email, default to `false` |
5859

5960
#### FileLogger.enableConsoleCapture()
6061

RNFileLogger.podspec

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ Pod::Spec.new do |s|
1616
s.source_files = "ios/**/*.{h,mm}"
1717
s.framework = "MessageUI"
1818

19-
s.dependency "CocoaLumberjack"
19+
s.dependency "CocoaLumberjack", "~> 3.8.5"
20+
s.dependency "SSZipArchive", "~> 2.4.3"
2021

2122
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
2223
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.

android/src/main/java/com/betomorrow/rnfilelogger/FileLoggerModule.java

+32-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
import java.io.File;
2020
import java.io.FilenameFilter;
21+
import java.io.FileOutputStream;
2122
import java.nio.charset.Charset;
2223
import java.util.ArrayList;
24+
import java.util.zip.ZipEntry;
25+
import java.util.zip.ZipOutputStream;
2326

2427
import ch.qos.logback.classic.Level;
2528
import ch.qos.logback.classic.LoggerContext;
@@ -185,6 +188,7 @@ public void sendLogFilesByEmail(ReadableMap options, Promise promise) {
185188
ReadableArray to = options.hasKey("to") ? options.getArray("to") : null;
186189
String subject = options.hasKey("subject") ? options.getString("subject") : null;
187190
String body = options.hasKey("body") ? options.getString("body") : null;
191+
boolean compressFiles = options.hasKey("compressFiles") && options.getBoolean("compressFiles");
188192

189193
Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE, Uri.parse("mailto:"));
190194
intent.setType("plain/text");
@@ -200,13 +204,38 @@ public void sendLogFilesByEmail(ReadableMap options, Promise promise) {
200204
}
201205

202206
ArrayList<Uri> uris = new ArrayList<>();
203-
for (File file : getLogFiles()) {
204-
Uri fileUri = FileProvider.getUriForFile(
207+
File[] logFiles = getLogFiles();
208+
209+
if (compressFiles && logFiles.length > 0) {
210+
// Create a zip file containing all log files
211+
File zipFile = new File(logsDirectory, "logs.zip");
212+
try (FileOutputStream fos = new FileOutputStream(zipFile);
213+
ZipOutputStream zos = new ZipOutputStream(fos)) {
214+
215+
for (File logFile : logFiles) {
216+
ZipEntry zipEntry = new ZipEntry(logFile.getName());
217+
zos.putNextEntry(zipEntry);
218+
java.nio.file.Files.copy(logFile.toPath(), zos);
219+
zos.closeEntry();
220+
}
221+
}
222+
223+
Uri zipUri = FileProvider.getUriForFile(
224+
reactContext,
225+
reactContext.getApplicationContext().getPackageName() + ".provider",
226+
zipFile);
227+
uris.add(zipUri);
228+
} else {
229+
// Send individual log files
230+
for (File file : logFiles) {
231+
Uri fileUri = FileProvider.getUriForFile(
205232
reactContext,
206233
reactContext.getApplicationContext().getPackageName() + ".provider",
207234
file);
208-
uris.add(fileUri);
235+
uris.add(fileUri);
236+
}
209237
}
238+
210239
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
211240
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
212241

example/App.tsx

+26-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { FileLogger, LogLevel } from "react-native-file-logger";
55
export const App = () => {
66
const [logLevel, setLogLevel] = useState(LogLevel.Debug);
77
const [enabled, setEnabled] = useState(true);
8+
const [count, setCount] = useState(0);
89

910
useEffect(() => {
10-
FileLogger.configure({ logLevel: LogLevel.Debug }).then(() => console.log("File-logger configured"));
11+
FileLogger.configure({ logLevel: LogLevel.Debug, maximumFileSize: 1024 }).then(() =>
12+
console.log("File-logger configured")
13+
);
1114
}, []);
1215

1316
const showLogFilePaths = async () => {
@@ -34,6 +37,7 @@ export const App = () => {
3437
3538
subject: "Log files",
3639
body: "Please find attached the log files from your app",
40+
compressFiles: true,
3741
});
3842
};
3943

@@ -67,13 +71,31 @@ export const App = () => {
6771
<View style={styles.container}>
6872
<View style={styles.buttonContainer}>
6973
<View style={styles.button}>
70-
<Button title="Log info" onPress={() => console.log("Log info", { nested: { data: 123 } })} />
74+
<Button
75+
title="Log info"
76+
onPress={() => {
77+
console.log("Log info", { nested: { count } });
78+
setCount(c => c + 1);
79+
}}
80+
/>
7181
</View>
7282
<View style={styles.button}>
73-
<Button title="Log warning" onPress={() => console.warn("Log warning", { nested: { data: 456 } })} />
83+
<Button
84+
title="Log warning"
85+
onPress={() => {
86+
console.warn("Log warning", { nested: { count } });
87+
setCount(c => c + 1);
88+
}}
89+
/>
7490
</View>
7591
<View style={styles.button}>
76-
<Button title="Log error" onPress={() => console.error("Log error", { nested: { data: 789 } })} />
92+
<Button
93+
title="Log error"
94+
onPress={() => {
95+
console.error("Log error", { nested: { count } });
96+
setCount(c => c + 1);
97+
}}
98+
/>
7799
</View>
78100
<View style={styles.button}>
79101
<Button title="Log large data" onPress={() => massiveLogging()} />

example/package-lock.json

+12-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ios/FileLogger.mm

+36-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#import <CocoaLumberjack/CocoaLumberjack.h>
55
#import <MessageUI/MessageUI.h>
66
#import "FileLoggerFormatter.h"
7+
#import <SSZipArchive/SSZipArchive.h>
78

89
enum LogLevel {
910
LOG_LEVEL_DEBUG,
@@ -87,7 +88,8 @@ - (dispatch_queue_t)methodQueue {
8788
NSArray<NSString*>* to = options[@"to"];
8889
NSString* subject = options[@"subject"];
8990
NSString* body = options[@"body"];
90-
91+
NSNumber* compressFiles = options[@"compressFiles"];
92+
9193
if (![MFMailComposeViewController canSendMail]) {
9294
reject(@"CannotSendMail", @"Cannot send emails on this device", nil);
9395
return;
@@ -106,9 +108,27 @@ - (dispatch_queue_t)methodQueue {
106108
}
107109

108110
NSArray<NSString*>* logFiles = self.fileLogger.logFileManager.sortedLogFilePaths;
109-
for (NSString* logFile in logFiles) {
110-
NSData* data = [NSData dataWithContentsOfFile:logFile];
111-
[composeViewController addAttachmentData:data mimeType:@"text/plain" fileName:[logFile lastPathComponent]];
111+
112+
if ([compressFiles boolValue]) {
113+
// Create a temporary directory for the zip file
114+
NSString* tempDir = NSTemporaryDirectory();
115+
NSString* zipPath = [tempDir stringByAppendingPathComponent:@"logs.zip"];
116+
117+
// Create zip file containing all log files
118+
[SSZipArchive createZipFileAtPath:zipPath withFilesAtPaths:logFiles];
119+
120+
// Add the zip file as attachment
121+
NSData* zipData = [NSData dataWithContentsOfFile:zipPath];
122+
[composeViewController addAttachmentData:zipData mimeType:@"application/zip" fileName:@"logs.zip"];
123+
124+
// Clean up the temporary zip file
125+
[[NSFileManager defaultManager] removeItemAtPath:zipPath error:nil];
126+
} else {
127+
// Add each log file as a separate attachment
128+
for (NSString* logFile in logFiles) {
129+
NSData* data = [NSData dataWithContentsOfFile:logFile];
130+
[composeViewController addAttachmentData:data mimeType:@"text/plain" fileName:[logFile lastPathComponent]];
131+
}
112132
}
113133

114134
UIViewController* presentingViewController = UIApplication.sharedApplication.delegate.window.rootViewController;
@@ -134,28 +154,32 @@ - (void)mailComposeController:(MFMailComposeViewController*)controller didFinish
134154

135155
// Signature only used by the new architecture.
136156
- (void)configure:(JS::NativeFileLogger::NativeConfigureOptions &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
137-
NSString* logsDirectory = options.logsDirectory();
138157
NSMutableDictionary * optionsDict = [NSMutableDictionary dictionary];
158+
139159
[optionsDict setValue:@(options.dailyRolling()) forKey:@"dailyRolling"];
140160
[optionsDict setValue:@(options.maximumFileSize()) forKey:@"maximumFileSize"];
141161
[optionsDict setValue:@(options.maximumNumberOfFiles()) forKey:@"maximumNumberOfFiles"];
162+
NSString* logsDirectory = options.logsDirectory();
142163
if (logsDirectory) {
143164
[optionsDict setValue:logsDirectory forKey:@"logsDirectory"];
144165
}
166+
145167
[self configure:optionsDict resolver:resolve rejecter:reject];
146168
}
147169

148-
170+
// Signature only used by the new architecture.
149171
- (void)sendLogFilesByEmail:(JS::NativeFileLogger::SendByEmailOptions &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
150-
NSDictionary * optionsDict = @{
151-
@"subject": options.subject(),
152-
@"body": options.body(),
153-
@"to": convertToNSArray(options.to())
154-
};
172+
NSMutableDictionary * optionsDict = [NSMutableDictionary dictionary];
173+
174+
[optionsDict setValue:options.subject() forKey:@"subject"];
175+
[optionsDict setValue:options.body() forKey:@"body"];
176+
[optionsDict setValue:convertToNSArray(options.to()) forKey:@"to"];
177+
[optionsDict setValue:@(options.compressFiles()) forKey:@"compressFiles"];
178+
155179
[self sendLogFilesByEmail:optionsDict resolver:resolve rejecter:reject];
156180
}
157181

158-
182+
// Signature only used by the new architecture.
159183
- (void)write:(double)level msg:(NSString *)msg {
160184
NSNumber* _Nonnull logLevel = [NSNumber numberWithInt:level];
161185
[self write:logLevel str:msg];

src/NativeFileLogger.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type SendByEmailOptions = {
1212
to?: string[];
1313
subject?: string;
1414
body?: string;
15+
compressFiles: boolean;
1516
};
1617

1718
export interface Spec extends TurboModule {

src/index.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface SendByEmailOptions {
3030
to?: string | string[];
3131
subject?: string;
3232
body?: string;
33+
compressFiles?: boolean;
3334
}
3435

3536
class FileLoggerStatic {
@@ -134,12 +135,13 @@ class FileLoggerStatic {
134135
}
135136

136137
sendLogFilesByEmail(options: SendByEmailOptions = {}): Promise<void> {
137-
if (options.to) {
138-
const toEmails = Array.isArray(options.to) ? options.to : [options.to];
139-
return RNFileLogger.sendLogFilesByEmail({ ...options, to: toEmails });
140-
} else {
141-
return RNFileLogger.sendLogFilesByEmail({ ...options, to: undefined });
142-
}
138+
const { to, subject, body, compressFiles = false } = options;
139+
return RNFileLogger.sendLogFilesByEmail({
140+
to: to ? (Array.isArray(to) ? to : [to]) : undefined,
141+
subject,
142+
body,
143+
compressFiles,
144+
});
143145
}
144146

145147
debug(msg: string) {

0 commit comments

Comments
 (0)