flutter开发实战-log日志存储zip上传,发送钉钉机器人消息
当我们需要Apk上传的时候,我们需要将日志打包并上传到七牛,上传之后通过钉钉通知我们日志下载地址。
这里我使用的是loggy来处理日志
一、引入loggy日志格式插件
在工程的pubspec.yaml中引入插件
loggy: ^2.0.2
使用loggy,当引入loggy后,可以在main方法中进行初始化
import 'package:loggy/loggy.dart';
main() {
Loggy.initLoggy();
}
之后在需要的地方进行输入相关日志
import 'package:loggy/loggy.dart';
class DoSomeWork {
DoSomeWork() {
logDebug('This is debug message');
logInfo('This is info message');
logWarning('This is warning message');
logError('This is error message');
}
}
二、日志存储到本地
查看loggy源码看到,日志没有存储到本地,在日志的目录下可以看到printers目录,LoggyPrinter为printer的抽象类
part of loggy;
/// Printer used to show logs, this can be easily swapped or replaced
abstract class LoggyPrinter {
const LoggyPrinter();
void onLog(LogRecord record);
}
当然我们需要通过继承该类来实现将日志内容写入到本地文件中,这时候我们定义了一个FilePrinter,实现onLog方法将日志写入文件中。
- 创建日志File
我们需要指定log日志所在目录,可以使用path_provider来获取document、tmp等目录。
Future<String> createDirectory(String appTAG) async {
final Directory directory = await getApplicationDocumentsDirectory();
var file = Directory("${directory.path}/$appTAG");
try {
bool exist = await file.exists();
if (exist == false) {
await file.create();
}
} catch (e) {
print("createDirectory error");
}
return file.path;
}
创建日志File
Future<void> getDirectoryForLogRecord() async {
String currentDay = getCurrentDay();
if (currentDay != _currentDate) {
final String fileDir = await createDirectory(this.appTAG);
file = File('$fileDir/$currentDay.log');
_sink = file!.openWrite(
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
encoding: encoding,
);
_currentDate = currentDay;
}
}
- 处理日志跨天的情况
每一天的日志对应一个date.log文件,如20240101.log
如果出现正好跨天的情况下,需要生成新的File来处理。这里定义了一个定时器,10分钟检测一次,如果日期不一致,则重新创建File
// 定时器
void startTimer() {
timerDispose();
_timer = Timer.periodic(dateCheckDuration!, (timer) {
getDirectoryForLogRecord();
});
}
void timerDispose() {
_timer?.cancel();
_timer = null;
}
- IOSink写入文件
写入文件,我们需要用到IOSink,
写入的文件file调用openWrite即可获得IOSink对象。
openWrite方法如下
IOSink openWrite({FileMode mode: FileMode.write, Encoding encoding: utf8});
默认情况下写入是会覆盖整个文件的,但是可以通过下面的方式来更改写入模式
IOSink ioSink = logFile.openWrite(mode: FileMode.append);
IOSink写入文件流程如下
var logFile = File('log.txt');
var sink = logFile.openWrite();
sink.write('FILE ACCESSED ${DateTime.now()}\n');
await sink.flush();
await sink.close();
通过onLog方法输入的record
@override
void onLog(LogRecord record) async {
_sink?.writeln('${record.time} [${record.level.toString().substring(0, 1)}] ${record.loggerName}: ${record.message}');
}
通过sink将文件保存到文件中。
完整的FilePrinter如下
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:loggy/loggy.dart';
import 'package:path_provider/path_provider.dart';
import 'package:common_utils/common_utils.dart';
Future<String?> getDirectory(String appTAG) async {
final Directory directory = await getApplicationDocumentsDirectory();
var file = Directory("${directory.path}/$appTAG");
try {
bool exist = await file.exists();
if (exist == true) {
return file.path;
}
} catch (e) {
print("createDirectory error");
}
return null;
}
Future<String> createDirectory(String appTAG) async {
final Directory directory = await getApplicationDocumentsDirectory();
var file = Directory("${directory.path}/$appTAG");
try {
bool exist = await file.exists();
if (exist == false) {
await file.create();
}
} catch (e) {
print("createDirectory error");
}
return file.path;
}
// 输出的文本文件, 开启定时器处理跨天的log存储问题
class FilePrinter extends LoggyPrinter {
final bool overrideExisting;
final Encoding encoding;
final String appTAG;
// 检查日期时长,可能出现跨天的情况,比如十分钟检测一次,
Duration? dateCheckDuration;
IOSink? _sink;
File? file;
String? _currentDate;
// 定时器
Timer? _timer;
FilePrinter(
this.appTAG, {
this.overrideExisting = false,
this.encoding = utf8,
this.dateCheckDuration = const Duration(minutes: 10),
}) {
directoryLogRecord(onCallback: () {
// 开启定时器
startTimer();
});
}
void directoryLogRecord({required Function onCallback}) {
getDirectoryForLogRecord().whenComplete(() {
onCallback();
});
}
Future<void> getDirectoryForLogRecord() async {
String currentDay = getCurrentDay();
if (currentDay != _currentDate) {
final String fileDir = await createDirectory(this.appTAG);
file = File('$fileDir/$currentDay.log');
_sink = file!.openWrite(
mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend,
encoding: encoding,
);
_currentDate = currentDay;
}
}
String getCurrentDay() {
String currentDate =
DateUtil.formatDate(DateTime.now(), format: "yyyyMMdd");
return currentDate;
}
// 文件删除后重新设置log
Future<void> resetLogFile() async {
_currentDate = null;
getDirectoryForLogRecord();
}
@override
void onLog(LogRecord record) async {
_sink?.writeln('${record.time} [${record.level.toString().substring(0, 1)}] ${record.loggerName}: ${record.message}');
}
dispose() {
timerDispose();
}
// 定时器
void startTimer() {
timerDispose();
_timer = Timer.periodic(dateCheckDuration!, (timer) {
getDirectoryForLogRecord();
});
}
void timerDispose() {
_timer?.cancel();
_timer = null;
}
}
// 输出到ConsolePrinter
class ConsolePrinter extends LoggyPrinter {
const ConsolePrinter() : super();
@override
void onLog(LogRecord record) {
print('${record.time} [${record.level.toString().substring(0, 1)}] ${record.loggerName}: ${record.message}');
}
}
// 多种同时使用的printer
class MultiPrinter extends LoggyPrinter {
MultiPrinter({
required this.consolePrinter,
required this.filePrinter,
});
final LoggyPrinter consolePrinter;
final LoggyPrinter filePrinter;
@override
void onLog(LogRecord record) {
consolePrinter.onLog(record);
filePrinter.onLog(record);
}
}
三、日志log压缩成zip
将日志log压缩成zip,打包成zip时候,我们需要用到archive插件
在工程的pubspec.yaml中引入插件
archive: ^3.3.7
archive是一个Dart库,用于对各种存档和压缩格式进行编码和解码。
该archive使用示例如下
import 'dart:io';
import 'package:archive/archive_io.dart';
Future<void> main() async {
// Read the Zip file from disk.
final bytes = File('test.zip').readAsBytesSync();
// Decode the Zip file
final archive = ZipDecoder().decodeBytes(bytes);
// Extract the contents of the Zip archive to disk.
for (final file in archive) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
File('out/$filename')
..createSync(recursive: true)
..writeAsBytesSync(data);
} else {
Directory('out/$filename').createSync(recursive: true);
}
}
// Encode the archive as a BZip2 compressed Tar file.
final tarData = TarEncoder().encode(archive);
final tarBz2 = BZip2Encoder().encode(tarData);
// Write the compressed tar file to disk.
final fp = File('test.tbz');
fp.writeAsBytesSync(tarBz2);
// Zip a directory to out.zip using the zipDirectory convenience method
var encoder = ZipFileEncoder();
await encoder.zipDirectoryAsync(Directory('out'), filename: 'out.zip');
// Manually create a zip of a directory and individual files.
encoder.create('out2.zip');
await encoder.addDirectory(Directory('out'));
await encoder.addFile(File('test.zip'));
encoder.closeSync();
}
压缩log,我这里创建一个log_archive类
- 首先创建zip目录
Future<String?> getZipDir() async {
final Directory directory = await getApplicationDocumentsDirectory();
var file = Directory("${directory.path}/zip");
try {
bool exist = await file.exists();
if (exist == false) {
await file.create();
}
} catch (e) {
print("createDirectory error");
}
return file.path;
}
Future<void> setZipPath({String? zipName}) async {
if (!(zipName != null && zipName.isNotEmpty)) {
String currentTime =
DateUtil.formatDate(DateTime.now(), format: "yyyy_MM_dd_HH_mm_ss");
zipName = "$currentTime.zip";
}
if (!zipName.endsWith(".zip")) {
zipName = "$zipName.zip";
}
String? zipDir = await getZipDir();
if (zipDir != null && zipDir.isNotEmpty) {
String zipPath = "${zipDir}/${zipName}";
this.zipPath = zipPath;
}
}
- 创建zip文件
创建zip文件,需要用到ZipFileEncoder,如果有同名的zip文件,则删除后重新生成新的zip文件
Future<void> createZip() async {
if (!(zipPath != null && zipPath!.isNotEmpty)) {
return;
}
bool fileExists = await checkFileExists();
if (fileExists == true) {
// 文件存在
// 删除后重新创建
File file = File(zipPath!);
await file.delete();
}
// zip文件重新生成
zipFileEncoder.open(zipPath!);
}
- 添加File文件
zipFileEncoder生成zip后,添加File问价
Future<void> addFile(File file) async {
bool fileExists = await checkFileExists();
if (fileExists) {
await zipFileEncoder.addFile(file);
await close();
}
}
- 最后调用close
zipFileEncoder添加File后,需要结束编码并关闭
Future<void> close() async {
zipFileEncoder.close();
}
log_archive完整代码如下
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:archive/archive_io.dart';
import 'package:common_utils/common_utils.dart';
import 'package:path_provider/path_provider.dart';
Future<String?> getZipDir() async {
final Directory directory = await getApplicationDocumentsDirectory();
var file = Directory("${directory.path}/zip");
try {
bool exist = await file.exists();
if (exist == false) {
await file.create();
}
} catch (e) {
print("createDirectory error");
}
return file.path;
}
// archive
class LogArchive {
String? zipPath;
late ZipFileEncoder zipFileEncoder;
LogArchive() {
zipFileEncoder = ZipFileEncoder();
}
Future<void> setZipPath({String? zipName}) async {
if (!(zipName != null && zipName.isNotEmpty)) {
String currentTime =
DateUtil.formatDate(DateTime.now(), format: "yyyy_MM_dd_HH_mm_ss");
zipName = "$currentTime.zip";
}
if (!zipName.endsWith(".zip")) {
zipName = "$zipName.zip";
}
String? zipDir = await getZipDir();
if (zipDir != null && zipDir.isNotEmpty) {
String zipPath = "${zipDir}/${zipName}";
this.zipPath = zipPath;
}
}
Future<void> createZip() async {
if (!(zipPath != null && zipPath!.isNotEmpty)) {
return;
}
bool fileExists = await checkFileExists();
if (fileExists == true) {
// 文件存在
// 删除后重新创建
File file = File(zipPath!);
await file.delete();
}
// zip文件重新生成
zipFileEncoder.open(zipPath!);
}
Future<void> addFile(File file) async {
bool fileExists = await checkFileExists();
if (fileExists) {
await zipFileEncoder.addFile(file);
await close();
}
}
Future<void> addFiles(List<File> files) async {
bool fileExists = await checkFileExists();
if (fileExists) {
for (File file in files) {
await zipFileEncoder.addFile(file);
}
await close();
}
}
Future<void> close() async {
zipFileEncoder.close();
}
Future<bool> checkFileExists() async {
if (!(zipPath != null && zipPath!.isNotEmpty)) {
return false;
}
try {
File file = File(zipPath!);
bool exist = await file.exists();
if (exist == true) {
return true;
}
} catch (e) {
print("checkFileExists error");
}
return false;
}
// 删除单个zip文件
Future<void> zipDelete(String aZipPath) async {
if (aZipPath.isEmpty) {
return;
}
final File file = File(aZipPath);
bool exist = await file.exists();
if (exist == false) {
print("LogArchive 文件不存在");
return;
}
await file.delete();
}
// 清空zip目录
Future<void> zipClean() async {
String? zipDir = await getZipDir();
if (zipDir != null && zipDir.isNotEmpty) {
var dir = Directory(zipDir);
await dir.delete(recursive: true);
}
}
Future<void> readZip(String zipDir) async {
if (!(zipPath != null && zipPath!.isNotEmpty)) {
return;
}
// Read the Zip file from disk.
final File file = File(zipPath!);
bool exist = await file.exists();
if (exist == false) {
print("LogArchive 文件不存在");
return;
}
try {
// InputFileStream only uses a cache buffer memory (4k by default), not the entire file
var stream = InputFileStream(zipPath!);
// The archive will have the memory of the compressed archive. ArchiveFile's are decompressed on
// demand
var zip = ZipDecoder().decodeBuffer(stream);
for (var file in zip.files) {
final filename = file.name;
if (file.isFile) {
final data = file.content as List<int>;
final logFile = await File('${zipDir}/out/$filename')
..create(recursive: true)
..writeAsBytesSync(data);
String logContent = await logFile.readAsString(encoding: utf8);
print("readZip logContent:${logContent}");
} else {
await Directory('${zipDir}/out/' + filename).create(recursive: true);
}
// file.clear() will release the file's compressed memory
file.clear();
}
} catch(e) {
print("readZip e:${e.toString()}");
}
}
}
四、上传到七牛
文件上传的到七牛,需要用到七牛的qiniu_flutter_sdk插件
在工程的pubspec.yaml中引入插件
qiniu_flutter_sdk: ^0.5.0
通过使用该插件上传示例
// 创建 storage 对象
storage = Storage();
// 创建 Controller 对象
putController = PutController();
// 使用 storage 的 putFile 对象进行文件上传
storage.putFile(File('./file.txt'), 'TOKEN', PutOptions(
controller: putController,
))
- 七牛token获取
我这边使用定义个log_uploader类,由于七牛上传需要token,token需要从服务端获取,所以定义个一个抽象类LogTokenFetch
可以实现一个类来实现getToken。
// 定义获取token接口
abstract class LogTokenFetch {
Future<String> getToken();
}
// 下面是示例, 请勿使用
class LogTokenFetchImpl implements LogTokenFetch {
@override
Future<String> getToken() {
// TODO: implement getToken
return Future.value('');
}
}
- 文件上传到七牛
我这边使用定义个log_uploader类,本质上还是套用qiniu_flutter_sdk插件的实现。
import 'dart:convert';
import 'dart:io';
import 'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart';
import 'package:crypto/crypto.dart';
import 'log_token_fetch.dart';
// logUploader
class LogUploader {
// 创建 storage 对象
final Storage storage = Storage();
final PutController putController = PutController();
LogTokenFetch? tokenFetch;
LogUploader(this.tokenFetch) {
init();
}
init() {
// 添加状态监听
putController.addStatusListener((StorageStatus status) {
print('LogUploader status:$status');
});
}
Future<String?> uploadFile(File zipFile, {String? customKey}) async {
if (tokenFetch != null) {
String? token = await tokenFetch!.getToken();
if (token != null && token.isNotEmpty) {
print("token:${token}");
String key = customKey??md5.convert(utf8.encode(zipFile.path)).toString();
PutResponse putResponse = await storage.putFile(zipFile, token, options: PutOptions(
key: key,
controller: putController,
));
return putResponse.key;
} else {
return null;
}
} else {
return null;
}
}
}
上传七牛过程中,可能会出现一下错误
StorageError [StorageErrorType.RESPONSE, 403]: {error: limited mimeType: this file type (application/octet-stream) is forbidden to upload}
当然需要确认token是否支持了对应的mimeType类型。
五、发送消息到钉钉机器人
经常会遇到钉钉的机器人消息,我们也可以调用其api实现发送消息。
请查考网址:https://open.dingtalk.com/document/orgapp/custom-robots-send-group-messages
这里使用的是插件dingtalk_bot_sender插件,当然dingtalk_bot_sender源码也是http请求实现了发送消息到钉钉机器人的API接口。
在工程的pubspec.yaml中引入插件
dingtalk_bot_sender: ^1.2.0
发送消息,使用的是DingTalkSender,我们可以发送markdown、url、文本等等
使用示例如下
final sender = DingTalkSender(
hookUrl: hookUrl,
keyword: keyword,
appsecret: appsecret,
);
await sender.sendText('1');
final markdown = '''
markdown内容
''';
await sender.sendMarkdown(markdown);
我这边发送的是日志链接地址
final sender = DingTalkSender(
hookUrl: hookUrl,
keyword: keyword,
appsecret: appsecret,
);
await sender.sendLink(
title: "app日志", text: "下载地址:${url}", messageUrl: url);
到此,flutter开发实战-log日志存储zip上传,发送钉钉机器人消息完成。
六、小结
flutter开发实战-log日志存储zip上传,发送钉钉机器人消息。
学习记录,每天不停进步。
本文地址:https://brucegwo.blog.csdn.net/article/details/138672565
两款小游戏
战机长空 | 小绳套牛 |
---|---|