You've already forked joplin
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:
BIN
packages/app-cli/tests/support/onenote/onenote_desktop.one
Normal file
BIN
packages/app-cli/tests/support/onenote/onenote_desktop.one
Normal file
Binary file not shown.
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -210,4 +210,5 @@ IDPSSO
|
||||
nameid
|
||||
attrname
|
||||
dpkg
|
||||
ATTLIST
|
||||
onepkg
|
||||
ATTLIST
|
||||
|
||||
Reference in New Issue
Block a user