Harmony6.0实战16:获取文件大小和文件类型
HarmonyOS文件大小和类型获取是应用开发中的基础但重要功能。通过本文介绍的方案,开发者可以:准确获取文件信息:使用fs.statSync和fs.stat同步/异步获取处理外部文件:通过文件选择器选择后复制到沙箱再操作计算文件夹大小:递归遍历统计所有文件判断文件类型:通过扩展名、MIME类型、文件头多维度验证
问题现象
在HarmonyOS应用开发中,文件操作是基础但至关重要的功能。开发者经常面临这样的困惑:
-
文件大小获取不准确:应用显示的文件大小与系统文件管理器显示不一致
-
文件类型判断困难:如何准确判断用户选择的文件是图片、视频还是文档?
-
存储空间统计混乱:应用自身占用的空间与文件实际大小概念混淆
-
跨沙箱访问问题:无法直接获取非沙箱内文件的信息
典型错误场景:
// 错误尝试:直接获取非沙箱文件信息
let filePath = '/storage/emulated/0/Download/test.jpg';
let stat = fs.statSync(filePath); // 这里会失败!
console.info('文件大小: ' + stat.size);
实际开发中的痛点:
-
用户选择图片后,需要显示文件大小但获取失败
-
上传文件时需要限制文件类型和大小,但无法准确判断
-
清理缓存时,不知道哪些文件占用了大量空间
-
需要统计文件夹总大小,但遍历操作复杂且性能差
背景知识
在深入解决方案前,需要理解HarmonyOS文件系统的几个关键概念:
1. 应用沙箱机制
HarmonyOS采用严格的沙箱安全模型,每个应用只能访问自己的沙箱目录。这意味着:
-
直接访问外部文件受限:不能直接操作
/storage/emulated/0/等路径 -
需要权限申请:访问用户文件需要申请相应权限
-
文件复制必要:操作前需将文件复制到沙箱内
2. 文件信息获取API
HarmonyOS提供了两套文件信息获取接口:
同步接口:fs.statSync()
// 同步获取文件信息
let stat = fs.statSync(filePath);
console.info('文件大小: ' + stat.size);
console.info('修改时间: ' + stat.mtime);
异步接口:fs.stat()
// 异步获取文件信息
fs.stat(filePath).then((stat: fs.Stat) => {
console.info('文件大小: ' + stat.size);
}).catch((err: BusinessError) => {
console.error('获取失败: ' + err.message);
});
3. 文件类型判断原理
HarmonyOS当前没有直接获取文件类型的API,但可以通过以下方式间接判断:
-
文件扩展名:通过文件后缀名判断类型
-
MIME类型映射:建立扩展名与MIME类型的对应关系
-
文件头信息:读取文件头部字节判断实际格式
4. 存储空间统计区别
开发者需要清楚区分两个概念:
-
应用占用空间:应用安装包、缓存、数据库等总和
-
文件实际大小:单个文件在磁盘上的实际占用空间
解决方案
方案一:基础文件信息获取
1. 获取单个文件大小(同步方式)
import fs from '@ohos.file.fs';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
/**
* 获取沙箱内文件大小(同步)
* @param context UIAbility上下文
* @param relativePath 沙箱内的相对路径
* @returns 文件大小(字节)
*/
function getFileSizeSync(context: common.UIAbilityContext, relativePath: string): number {
try {
// 构建沙箱内的完整路径
const sandboxPath = context.filesDir + relativePath;
// 同步获取文件信息
const stat = fs.statSync(sandboxPath);
if (stat.isFile()) {
console.info(`文件大小: ${stat.size} 字节 (${this.formatFileSize(stat.size)})`);
return stat.size;
} else {
console.error('路径指向的不是文件');
return 0;
}
} catch (error) {
const err = error as BusinessError;
console.error(`获取文件大小失败: ${err.message}, 错误码: ${err.code}`);
return 0;
}
}
/**
* 格式化文件大小显示
* @param bytes 字节数
* @returns 格式化后的字符串
*/
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
2. 获取单个文件大小(异步方式)
/**
* 获取沙箱内文件大小(异步)
* @param context UIAbility上下文
* @param relativePath 沙箱内的相对路径
* @returns Promise<number> 文件大小
*/
async function getFileSizeAsync(context: common.UIAbilityContext, relativePath: string): Promise<number> {
return new Promise((resolve, reject) => {
const sandboxPath = context.filesDir + relativePath;
fs.stat(sandboxPath).then((stat: fs.Stat) => {
if (stat.isFile()) {
console.info(`异步获取文件大小: ${stat.size} 字节`);
resolve(stat.size);
} else {
reject(new Error('路径指向的不是文件'));
}
}).catch((err: BusinessError) => {
console.error(`异步获取失败: ${err.message}`);
reject(err);
});
});
}
// 使用示例
async function exampleUsage() {
try {
const size = await getFileSizeAsync(context, '/documents/report.pdf');
console.info(`文件大小: ${this.formatFileSize(size)}`);
} catch (error) {
console.error('获取文件大小失败');
}
}
方案二:处理外部文件(如图片选择)
当用户从相册选择文件时,需要先复制到沙箱再获取信息:
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import fs from '@ohos.file.fs';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
/**
* 选择图片并获取文件信息
*/
export class FileInfoManager {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
/**
* 选择单张图片并获取信息
*/
async selectAndGetImageInfo(): Promise<{
size: number;
type: string;
sandboxPath: string;
}> {
try {
// 1. 配置图片选择器
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
// 2. 创建选择器并选择图片
const photoPicker = new photoAccessHelper.PhotoViewPicker();
const photoSelectResult = await photoPicker.select(photoSelectOptions);
if (!photoSelectResult || photoSelectResult.photoUris.length === 0) {
throw new Error('未选择图片');
}
const originalUri = photoSelectResult.photoUris[0];
// 3. 复制到沙箱
const sandboxPath = await this.copyToSandbox(originalUri, 'selected_image.jpg');
// 4. 获取文件信息
const fileInfo = await this.getFileInfo(sandboxPath);
return {
size: fileInfo.size,
type: this.getFileType(sandboxPath),
sandboxPath: sandboxPath
};
} catch (error) {
const err = error as BusinessError;
console.error(`选择图片失败: ${err.message}, 错误码: ${err.code}`);
throw error;
}
}
/**
* 复制文件到沙箱
*/
private async copyToSandbox(originalUri: string, fileName: string): Promise<string> {
// 创建目标目录
const targetDir = this.context.filesDir + '/temp/';
await fs.mkdir(targetDir);
const targetPath = targetDir + fileName;
// 打开源文件和目标文件
const srcFile = fs.openSync(originalUri, fs.OpenMode.READ_ONLY);
const destFile = fs.openSync(targetPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
try {
// 执行复制
fs.copyFileSync(srcFile.fd, destFile.fd);
console.info(`文件复制成功: ${targetPath}`);
return targetPath;
} finally {
// 关闭文件句柄
fs.closeSync(srcFile);
fs.closeSync(destFile);
}
}
/**
* 获取文件详细信息
*/
private async getFileInfo(filePath: string): Promise<fs.Stat> {
return new Promise((resolve, reject) => {
fs.stat(filePath).then(resolve).catch(reject);
});
}
/**
* 判断文件类型
*/
private getFileType(filePath: string): string {
return this.getPathExtension(filePath).toLowerCase();
}
/**
* 提取文件扩展名
*/
private getPathExtension(filePath: string): string {
const lastDotIndex = filePath.lastIndexOf('.');
if (lastDotIndex < 0) {
return 'unknown';
}
return filePath.slice(lastDotIndex + 1);
}
}
方案三:获取文件夹总大小
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
/**
* 文件夹大小计算器
*/
export class FolderSizeCalculator {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
/**
* 获取文件夹总大小(递归计算)
*/
getFolderSize(folderPath: string): number {
let totalSize = 0;
try {
// 配置遍历选项
const listFileOption: fs.ListFileOptions = {
recursion: true, // 递归遍历子目录
listNum: 0, // 0表示获取所有文件
filter: { // 过滤条件(可选)
suffix: ['.jpg', '.png', '.pdf', '.docx'], // 只统计特定类型
displayName: ['*'] // 所有文件
}
};
// 获取文件列表
const fileNames = fs.listFileSync(folderPath, listFileOption);
// 计算总大小
for (let i = 0; i < fileNames.length; i++) {
const filePath = folderPath + (folderPath.endsWith('/') ? '' : '/') + fileNames[i];
const stat = fs.statSync(filePath);
if (stat.isFile()) {
totalSize += stat.size;
console.debug(`文件: ${fileNames[i]}, 大小: ${this.formatFileSize(stat.size)}`);
}
}
console.info(`文件夹总大小: ${this.formatFileSize(totalSize)}`);
return totalSize;
} catch (error) {
const err = error as BusinessError;
console.error(`计算文件夹大小失败: ${err.message}`);
return 0;
}
}
/**
* 异步获取文件夹大小
*/
async getFolderSizeAsync(folderPath: string): Promise<number> {
return new Promise((resolve, reject) => {
try {
const size = this.getFolderSize(folderPath);
resolve(size);
} catch (error) {
reject(error);
}
});
}
/**
* 获取应用缓存目录大小
*/
getCacheSize(): number {
const cacheDir = this.context.cacheDir;
return this.getFolderSize(cacheDir);
}
/**
* 清理指定大小的旧文件
*/
async cleanupOldFiles(folderPath: string, maxSizeMB: number): Promise<void> {
const maxSizeBytes = maxSizeMB * 1024 * 1024;
const currentSize = this.getFolderSize(folderPath);
if (currentSize <= maxSizeBytes) {
console.info('文件夹大小未超过限制,无需清理');
return;
}
console.info(`开始清理,当前大小: ${this.formatFileSize(currentSize)}, 限制: ${maxSizeMB}MB`);
// 获取文件列表并按修改时间排序
const files = this.getFilesSortedByMtime(folderPath);
let deletedSize = 0;
const targetSize = currentSize - maxSizeBytes;
for (const file of files) {
if (deletedSize >= targetSize) {
break;
}
try {
const fileSize = fs.statSync(file.path).size;
fs.unlinkSync(file.path);
deletedSize += fileSize;
console.info(`已删除: ${file.name}, 释放: ${this.formatFileSize(fileSize)}`);
} catch (error) {
console.warn(`删除文件失败: ${file.name}`);
}
}
console.info(`清理完成,共释放: ${this.formatFileSize(deletedSize)}`);
}
/**
* 按修改时间获取文件列表(从旧到新)
*/
private getFilesSortedByMtime(folderPath: string): Array<{name: string, path: string, mtime: number}> {
const files: Array<{name: string, path: string, mtime: number}> = [];
const fileNames = fs.listFileSync(folderPath, { recursion: true, listNum: 0 });
for (const fileName of fileNames) {
const filePath = folderPath + (folderPath.endsWith('/') ? '' : '/') + fileName;
const stat = fs.statSync(filePath);
if (stat.isFile()) {
files.push({
name: fileName,
path: filePath,
mtime: stat.mtime
});
}
}
// 按修改时间升序排序(最旧的在前)
return files.sort((a, b) => a.mtime - b.mtime);
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
方案四:文件类型判断工具类
/**
* 文件类型判断工具
*/
export class FileTypeDetector {
// 常见文件类型映射
private static readonly FILE_TYPE_MAP: Record<string, string> = {
// 图片类型
'jpg': 'image', 'jpeg': 'image', 'png': 'image', 'gif': 'image',
'bmp': 'image', 'webp': 'image', 'svg': 'image', 'ico': 'image',
// 视频类型
'mp4': 'video', 'avi': 'video', 'mov': 'video', 'wmv': 'video',
'flv': 'video', 'mkv': 'video', 'webm': 'video',
// 音频类型
'mp3': 'audio', 'wav': 'audio', 'aac': 'audio', 'flac': 'audio',
'ogg': 'audio', 'm4a': 'audio',
// 文档类型
'pdf': 'document', 'doc': 'document', 'docx': 'document',
'xls': 'document', 'xlsx': 'document', 'ppt': 'document',
'pptx': 'document', 'txt': 'document', 'md': 'document',
// 压缩文件
'zip': 'archive', 'rar': 'archive', '7z': 'archive',
'tar': 'archive', 'gz': 'archive',
// 代码文件
'js': 'code', 'ts': 'code', 'java': 'code', 'py': 'code',
'cpp': 'code', 'html': 'code', 'css': 'code', 'json': 'code',
'xml': 'code'
};
// MIME类型映射
private static readonly MIME_TYPE_MAP: Record<string, string> = {
'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif',
'image/webp': 'webp', 'image/svg+xml': 'svg',
'video/mp4': 'mp4', 'video/avi': 'avi', 'video/quicktime': 'mov',
'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/aac': 'aac',
'application/pdf': 'pdf', 'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
'application/zip': 'zip', 'application/x-rar-compressed': 'rar'
};
/**
* 通过文件扩展名判断类型
*/
static getFileTypeByExtension(filePath: string): string {
const extension = this.getFileExtension(filePath).toLowerCase();
return this.FILE_TYPE_MAP[extension] || 'unknown';
}
/**
* 通过MIME类型判断
*/
static getFileTypeByMime(mimeType: string): string {
const extension = this.MIME_TYPE_MAP[mimeType];
if (extension) {
return this.FILE_TYPE_MAP[extension] || 'unknown';
}
return 'unknown';
}
/**
* 获取详细文件类型信息
*/
static getFileTypeInfo(filePath: string): {
category: string; // 大类:image, video, audio, document等
extension: string; // 扩展名
mimeType: string; // MIME类型
description: string; // 描述
} {
const extension = this.getFileExtension(filePath).toLowerCase();
const category = this.FILE_TYPE_MAP[extension] || 'unknown';
// 根据扩展名推测MIME类型
let mimeType = 'application/octet-stream';
for (const [mime, ext] of Object.entries(this.MIME_TYPE_MAP)) {
if (ext === extension) {
mimeType = mime;
break;
}
}
// 类型描述
const descriptions: Record<string, string> = {
'image': '图片文件',
'video': '视频文件',
'audio': '音频文件',
'document': '文档文件',
'archive': '压缩文件',
'code': '代码文件',
'unknown': '未知文件'
};
return {
category: category,
extension: extension,
mimeType: mimeType,
description: descriptions[category] || '未知类型'
};
}
/**
* 检查文件是否属于指定类型
*/
static isFileType(filePath: string, expectedTypes: string[]): boolean {
const fileType = this.getFileTypeByExtension(filePath);
return expectedTypes.includes(fileType);
}
/**
* 获取文件扩展名
*/
private static getFileExtension(filePath: string): string {
const lastDotIndex = filePath.lastIndexOf('.');
if (lastDotIndex < 0 || lastDotIndex === filePath.length - 1) {
return '';
}
return filePath.slice(lastDotIndex + 1);
}
/**
* 通过文件头判断实际文件类型(更准确)
*/
static async getRealFileType(filePath: string): Promise<string> {
try {
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
// 读取文件头(前4个字节通常足够判断)
const buffer = new ArrayBuffer(4);
const readSize = fs.readSync(file.fd, buffer);
fs.closeSync(file);
if (readSize < 4) {
return 'unknown';
}
const header = new Uint8Array(buffer);
// 检查常见文件类型的魔数
if (header[0] === 0xFF && header[1] === 0xD8 && header[2] === 0xFF) {
return 'image'; // JPEG
} else if (header[0] === 0x89 && header[1] === 0x50 && header[2] === 0x4E && header[3] === 0x47) {
return 'image'; // PNG
} else if (header[0] === 0x47 && header[1] === 0x49 && header[2] === 0x46) {
return 'image'; // GIF
} else if (header[0] === 0x25 && header[1] === 0x50 && header[2] === 0x44 && header[3] === 0x46) {
return 'document'; // PDF
} else if (header[0] === 0x50 && header[1] === 0x4B && header[2] === 0x03 && header[3] === 0x04) {
return 'archive'; // ZIP
}
return 'unknown';
} catch (error) {
console.error('读取文件头失败:', error);
return 'unknown';
}
}
}
完整示例:文件信息管理组件
@Entry
@Component
struct FileInfoDemo {
@State fileSize: string = '0 B';
@State fileType: string = '未知';
@State folderSize: string = '0 B';
@State selectedFile: string = '';
private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
private fileManager: FileInfoManager = new FileInfoManager(this.context);
private folderCalculator: FolderSizeCalculator = new FolderSizeCalculator(this.context);
build() {
Column({ space: 20 }) {
// 标题
Text('文件信息管理演示')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
.width('100%')
.textAlign(TextAlign.Center)
.margin({ top: 30, bottom: 20 });
// 文件选择区域
Column({ space: 10 }) {
Text('选择文件获取信息')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#222222');
Button('选择图片文件')
.width('80%')
.height(48)
.fontSize(16)
.onClick(async () => {
try {
const fileInfo = await this.fileManager.selectAndGetImageInfo();
this.fileSize = this.formatFileSize(fileInfo.size);
this.fileType = FileTypeDetector.getFileTypeInfo(fileInfo.sandboxPath).description;
this.selectedFile = fileInfo.sandboxPath;
console.info(`文件信息: 大小=${this.fileSize}, 类型=${this.fileType}`);
} catch (error) {
console.error('选择文件失败:', error);
}
});
if (this.selectedFile) {
Text(`已选择: ${this.selectedFile}`)
.fontSize(12)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
}
}
.width('90%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#20000000' });
// 文件信息展示
Column({ space: 15 }) {
Text('文件信息')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#222222');
Row({ space: 10 }) {
Text('文件大小:')
.fontSize(14)
.fontColor('#666666')
.width(80);
Text(this.fileSize)
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium);
}
Row({ space: 10 }) {
Text('文件类型:')
.fontSize(14)
.fontColor('#666666')
.width(80);
Text(this.fileType)
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium);
}
}
.width('90%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#20000000' });
// 文件夹大小计算
Column({ space: 15 }) {
Text('文件夹统计')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#222222');
Row({ space: 10 }) {
Text('缓存目录大小:')
.fontSize(14)
.fontColor('#666666')
.width(120);
Text(this.folderSize)
.fontSize(14)
.fontColor('#333333')
.fontWeight(FontWeight.Medium);
}
Button('计算缓存大小')
.width('60%')
.height(40)
.fontSize(14)
.onClick(() => {
const size = this.folderCalculator.getCacheSize();
this.folderSize = this.formatFileSize(size);
});
Button('清理缓存(超过10MB)')
.width('60%')
.height(40)
.fontSize(14)
.backgroundColor('#FF5722')
.fontColor(Color.White)
.onClick(async () => {
await this.folderCalculator.cleanupOldFiles(this.context.cacheDir, 10);
const newSize = this.folderCalculator.getCacheSize();
this.folderSize = this.formatFileSize(newSize);
});
}
.width('90%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#20000000' });
// 文件类型检测
Column({ space: 15 }) {
Text('文件类型检测')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#222222');
if (this.selectedFile) {
const typeInfo = FileTypeDetector.getFileTypeInfo(this.selectedFile);
Column({ space: 8 }) {
Row({ space: 10 }) {
Text('文件类别:')
.fontSize(14)
.fontColor('#666666')
.width(80);
Text(typeInfo.category)
.fontSize(14)
.fontColor('#333333');
}
Row({ space: 10 }) {
Text('扩展名:')
.fontSize(14)
.fontColor('#666666')
.width(80);
Text(typeInfo.extension)
.fontSize(14)
.fontColor('#333333');
}
Row({ space: 10 }) {
Text('MIME类型:')
.fontSize(14)
.fontColor('#666666')
.width(80);
Text(typeInfo.mimeType)
.fontSize(14)
.fontColor('#333333')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
}
}
} else {
Text('请先选择文件')
.fontSize(14)
.fontColor('#999999')
.fontStyle(FontStyle.Italic);
}
}
.width('90%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 4, color: '#20000000' });
}
.width('100%')
.height('100%')
.padding(20)
.backgroundColor('#F5F5F5')
.alignItems(HorizontalAlign.Center);
}
private formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
常见FAQ
Q1: 为什么获取的文件大小与系统显示不一致?
A: 这通常是由于以下原因:
-
单位换算差异:系统可能使用1000进制(1KB=1000B),而代码使用1024进制
-
文件系统开销:文件系统会有额外的元数据占用空间
-
稀疏文件:某些文件系统支持稀疏文件,实际占用与逻辑大小不同
解决方案:
// 使用正确的单位换算
function formatFileSizeHumanReadable(bytes: number, useDecimal: boolean = false): string {
const unit = useDecimal ? 1000 : 1024;
if (bytes < unit) return bytes + ' B';
const units = useDecimal
? ['KB', 'MB', 'GB', 'TB']
: ['KiB', 'MiB', 'GiB', 'TiB'];
const exp = Math.floor(Math.log(bytes) / Math.log(unit));
const size = bytes / Math.pow(unit, exp);
return size.toFixed(2) + ' ' + units[exp - 1];
}
Q2: 如何获取文件的创建时间?
A: 使用fs.stat返回的birthtime属性:
const stat = fs.statSync(filePath);
console.info('创建时间:', new Date(stat.birthtime).toLocaleString());
console.info('修改时间:', new Date(stat.mtime).toLocaleString());
console.info('访问时间:', new Date(stat.atime).toLocaleString());
注意:atime表示最后访问时间,ctime表示状态变更时间,birthtime才是创建时间。
Q3: 链接文件信息怎么获取?
A: 使用fs.lstat获取链接文件本身的信息,而不是指向的文件:
// 获取符号链接信息
const linkStat = fs.lstatSync('/path/to/symlink');
console.info('链接文件大小:', linkStat.size);
// 获取链接指向的文件信息
const targetStat = fs.statSync('/path/to/symlink');
console.info('目标文件大小:', targetStat.size);
Q4: 如何设置文件的最后访问时间?
A: 使用fs.utimes或fs.futimes:
// 修改文件的访问和修改时间
const now = Date.now();
fs.utimesSync(filePath, now, now);
// 或者使用文件描述符
const fd = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
fs.futimesSync(fd, now, now);
fs.closeSync(fd);
Q5: 大文件夹遍历性能优化
A: 对于包含大量文件的文件夹,建议:
-
分页遍历:使用
listNum参数限制每次获取的数量 -
异步处理:使用
listFile异步接口避免阻塞UI -
增量统计:定期更新统计结果,避免一次性计算
async function getLargeFolderSize(folderPath: string): Promise<number> {
let totalSize = 0;
let offset = 0;
const pageSize = 100; // 每页100个文件
while (true) {
const options: fs.ListFileOptions = {
recursion: true,
listNum: pageSize,
startOffset: offset
};
const files = await fs.listFile(folderPath, options);
if (files.length === 0) {
break;
}
// 批量处理文件
for (const file of files) {
const stat = await fs.stat(folderPath + '/' + file);
if (stat.isFile()) {
totalSize += stat.size;
}
}
offset += files.length;
// 每处理一页更新一次UI
this.updateProgress(offset, totalSize);
}
return totalSize;
}
Q6: 如何判断文件是否被其他进程占用?
A: 尝试以独占模式打开文件:
function isFileLocked(filePath: string): boolean {
try {
// 尝试以独占模式打开
const fd = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.EXCLUSIVE);
fs.closeSync(fd);
return false; // 文件未被占用
} catch (error) {
const err = error as BusinessError;
if (err.code === 13900015) { // EBUSY: 文件被占用
return true;
}
throw error;
}
}
Q7: 文件类型判断不准确怎么办?
A: 扩展名判断可能被伪造,建议结合多种方式:
-
文件头验证:读取文件前几个字节判断魔数
-
MIME类型检测:使用系统提供的MIME类型检测
-
文件内容分析:对特定格式进行深度解析
async function verifyFileType(filePath: string, expectedType: string): Promise<boolean> {
// 方法1:扩展名检查
const extType = FileTypeDetector.getFileTypeByExtension(filePath);
// 方法2:文件头检查
const realType = await FileTypeDetector.getRealFileType(filePath);
// 方法3:文件大小验证(如图片最小尺寸)
const stat = fs.statSync(filePath);
if (expectedType === 'image' && stat.size < 100) {
return false; // 图片文件太小,可能是伪造的
}
return extType === expectedType && realType === expectedType;
}
总结
HarmonyOS文件大小和类型获取是应用开发中的基础但重要功能。通过本文介绍的方案,开发者可以:
-
准确获取文件信息:使用
fs.statSync和fs.stat同步/异步获取 -
处理外部文件:通过文件选择器选择后复制到沙箱再操作
-
计算文件夹大小:递归遍历统计所有文件
-
判断文件类型:通过扩展名、MIME类型、文件头多维度验证
最佳实践建议:
-
✅ 始终在沙箱内操作文件,确保安全性
-
✅ 对大文件夹使用分页遍历,避免性能问题
-
✅ 结合多种方式验证文件类型,防止伪造
-
✅ 使用异步接口处理大文件,避免阻塞UI
-
✅ 定期清理临时文件,管理存储空间
通过合理的文件管理策略,你的HarmonyOS应用将能更好地处理用户文件,提供更流畅的体验。记住:好的文件管理不仅是功能实现,更是用户体验的重要组成部分。
更多推荐


所有评论(0)