1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-23 22:36:32 +02:00

Desktop: OneNote importer: Support directly importing .one files and, on Windows, .onepkg files (#13474)

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
This commit is contained in:
Henry Heino
2025-10-18 01:47:35 -07:00
committed by GitHub
parent 3097c3e589
commit 28eb53bd9f
7 changed files with 112 additions and 19 deletions

View File

@@ -21,11 +21,16 @@ export interface RemoveOptions {
recursive?: boolean;
}
export interface ZipExtractOptions {
export interface ArchiveExtractOptions {
source: string;
extractTo: string;
}
export interface CabExtractOptions extends ArchiveExtractOptions {
// Only files matching the pattern will be extracted
fileNamePattern: string;
}
export interface ZipEntry {
entryName: string;
name: string;
@@ -268,8 +273,11 @@ export default class FsDriverBase {
throw new Error('Not implemented: tarCreate');
}
public async zipExtract(_options: ZipExtractOptions): Promise<ZipEntry[]> {
public async zipExtract(_options: ArchiveExtractOptions): Promise<ZipEntry[]> {
throw new Error('Not implemented: zipExtract');
}
public async cabExtract(_options: CabExtractOptions) {
throw new Error('Not implemented: cabExtract.');
}
}

View File

@@ -1,6 +1,8 @@
import AdmZip = require('adm-zip');
import FsDriverBase, { Stat, ZipEntry, ZipExtractOptions } from './fs-driver-base';
import FsDriverBase, { Stat, ZipEntry, ArchiveExtractOptions, CabExtractOptions } from './fs-driver-base';
import time from './time';
import { execCommand } from '@joplin/utils';
import { extname } from 'path';
const md5File = require('md5-file');
const fs = require('fs-extra');
@@ -211,9 +213,30 @@ export default class FsDriverNode extends FsDriverBase {
await require('tar').create(options, filePaths);
}
public async zipExtract(options: ZipExtractOptions): Promise<ZipEntry[]> {
public async zipExtract(options: ArchiveExtractOptions): Promise<ZipEntry[]> {
const zip = new AdmZip(options.source);
zip.extractAllTo(options.extractTo, false);
return zip.getEntries();
}
public async cabExtract(options: CabExtractOptions) {
if (process.platform !== 'win32') {
throw new Error('Extracting CAB archives is only supported on Windows.');
}
const source = this.resolve(options.source);
const extractTo = this.resolve(options.extractTo);
if (extname(source).toLowerCase() !== '.cab') {
throw new Error(`Invalid file extension. Expected .CAB. Was ${extname(source)}`);
}
// See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/expand
await execCommand([
'expand.exe',
source,
`-f:${options.fileNamePattern}`,
extractTo,
], { quiet: true });
}
}

View File

@@ -142,7 +142,12 @@ export default class InteropService {
makeImportModule({
format: 'zip',
fileExtensions: ['zip'],
fileExtensions: [
'zip',
'one',
// .onepkg is a CAB archive, which Joplin can currently only extract on Windows
...(shim.isWindows() ? ['onepkg'] : []),
],
sources: [FileSystemItem.File],
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
description: _('OneNote Notebook'),

View File

@@ -257,4 +257,18 @@ describe('InteropService_Importer_OneNote', () => {
BaseModel.setIdGenerator(originalIdGenerator);
});
it('should support directly importing .one files', async () => {
const notes = await importNote(`${supportDir}/onenote/onenote_desktop.one`);
// For this test, just check that the extracted files exist.
expect(notes.map(note => note.title).sort()).toEqual([
// The three pages contained within the notebook
'Another page',
'Page 3',
'Test',
// The index page
'onenote_desktop',
]);
});
});

View File

@@ -4,7 +4,7 @@ import InteropService_Importer_Base from './InteropService_Importer_Base';
import { NoteEntity } from '../database/types';
import { rtrimSlashes } from '../../path-utils';
import InteropService_Importer_Md from './InteropService_Importer_Md';
import { join, resolve, normalize, sep, dirname, extname } from 'path';
import { join, resolve, normalize, sep, dirname, extname, basename } from 'path';
import Logger from '@joplin/utils/Logger';
import { uuidgen } from '../../uuid';
import shim from '../../shim';
@@ -41,34 +41,67 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
return normalize(withoutBasePath).split(sep)[0];
}
public async exec(result: ImportExportResult) {
private async extractFiles_(sourcePath: string, targetPath: string) {
const fileExtension = extname(sourcePath).toLowerCase();
const fileNameNoExtension = basename(sourcePath, extname(sourcePath));
if (fileExtension === '.zip') {
logger.info('Unzipping files...');
await shim.fsDriver().zipExtract({ source: sourcePath, extractTo: targetPath });
} else if (fileExtension === '.one') {
logger.info('Copying file...');
const outputDirectory = join(targetPath, fileNameNoExtension);
await shim.fsDriver().mkdir(outputDirectory);
await shim.fsDriver().copy(sourcePath, join(outputDirectory, basename(sourcePath)));
} else if (fileExtension === '.onepkg') {
// Change the file extension so that the archive can be extracted
const archivePath = join(targetPath, `${fileNameNoExtension}.cab`);
await shim.fsDriver().copy(sourcePath, archivePath);
const extractPath = join(targetPath, fileNameNoExtension);
await shim.fsDriver().mkdir(extractPath);
await shim.fsDriver().cabExtract({
source: archivePath,
extractTo: extractPath,
// Only the .one files are used--there's no need to extract
// other files.
fileNamePattern: '*.one',
});
} else {
throw new Error(`Unknown file extension: ${fileExtension}`);
}
return await shim.fsDriver().readDirStats(targetPath, { recursive: true });
}
private async execImpl_(result: ImportExportResult, unzipTempDirectory: string, tempOutputDirectory: string) {
const sourcePath = rtrimSlashes(this.sourcePath_);
const unzipTempDirectory = await this.temporaryDirectory_(true);
logger.info('Unzipping files...');
const files = await shim.fsDriver().zipExtract({ source: sourcePath, extractTo: unzipTempDirectory });
const files = await this.extractFiles_(sourcePath, unzipTempDirectory);
if (files.length === 0) {
result.warnings.push('Zip file has no files.');
return result;
}
const tempOutputDirectory = await this.temporaryDirectory_(true);
const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].entryName);
const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].path);
const notebookBaseDir = join(unzipTempDirectory, baseFolder, sep);
const outputDirectory2 = join(tempOutputDirectory, baseFolder);
const notebookFiles = files.filter(e => e.name !== '.onetoc2' && e.name !== 'OneNote_RecycleBin.onetoc2');
const notebookFiles = files.filter(e => {
return extname(e.path) !== '.onetoc2' && basename(e.path) !== 'OneNote_RecycleBin.onetoc2';
});
const { oneNoteConverter } = shim.requireDynamic('@joplin/onenote-converter');
logger.info('Extracting OneNote to HTML');
const skippedFiles = [];
for (const notebookFile of notebookFiles) {
const notebookFilePath = join(unzipTempDirectory, notebookFile.entryName);
const notebookFilePath = join(unzipTempDirectory, notebookFile.path);
// In some cases, the OneNote zip file can include folders and other files
// that shouldn't be imported directly. Skip these:
if (!['.one', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
logger.info('Skipping non-OneNote file:', notebookFile.entryName);
skippedFiles.push(notebookFile.entryName);
logger.info('Skipping non-OneNote file:', notebookFile.path);
skippedFiles.push(notebookFile.path);
continue;
}
@@ -94,14 +127,23 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
...this.options_,
format: 'html',
outputFormat: ImportModuleOutputFormat.Html,
});
logger.info('Finished');
result = await importer.exec(result);
return result;
}
public async exec(result: ImportExportResult) {
const unzipTempDirectory = await this.temporaryDirectory_(true);
const tempOutputDirectory = await this.temporaryDirectory_(true);
try {
return await this.execImpl_(result, unzipTempDirectory, tempOutputDirectory);
} finally {
await shim.fsDriver().remove(unzipTempDirectory);
await shim.fsDriver().remove(tempOutputDirectory);
}
}
private async moveSvgToLocalFile(baseFolder: string) {
const htmlFiles = await this.getValidHtmlFiles(resolve(baseFolder));

View File

@@ -210,4 +210,5 @@ IDPSSO
nameid
attrname
dpkg
onepkg
ATTLIST