You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +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;
|
recursive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ZipExtractOptions {
|
export interface ArchiveExtractOptions {
|
||||||
source: string;
|
source: string;
|
||||||
extractTo: string;
|
extractTo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CabExtractOptions extends ArchiveExtractOptions {
|
||||||
|
// Only files matching the pattern will be extracted
|
||||||
|
fileNamePattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ZipEntry {
|
export interface ZipEntry {
|
||||||
entryName: string;
|
entryName: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -268,8 +273,11 @@ export default class FsDriverBase {
|
|||||||
throw new Error('Not implemented: tarCreate');
|
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');
|
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 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 time from './time';
|
||||||
|
import { execCommand } from '@joplin/utils';
|
||||||
|
import { extname } from 'path';
|
||||||
const md5File = require('md5-file');
|
const md5File = require('md5-file');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
@@ -211,9 +213,30 @@ export default class FsDriverNode extends FsDriverBase {
|
|||||||
await require('tar').create(options, filePaths);
|
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);
|
const zip = new AdmZip(options.source);
|
||||||
zip.extractAllTo(options.extractTo, false);
|
zip.extractAllTo(options.extractTo, false);
|
||||||
return zip.getEntries();
|
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({
|
makeImportModule({
|
||||||
format: 'zip',
|
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],
|
sources: [FileSystemItem.File],
|
||||||
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
isNoteArchive: false, // Tells whether the file can contain multiple notes (eg. Enex or Jex format)
|
||||||
description: _('OneNote Notebook'),
|
description: _('OneNote Notebook'),
|
||||||
|
|||||||
@@ -257,4 +257,18 @@ describe('InteropService_Importer_OneNote', () => {
|
|||||||
|
|
||||||
BaseModel.setIdGenerator(originalIdGenerator);
|
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 { NoteEntity } from '../database/types';
|
||||||
import { rtrimSlashes } from '../../path-utils';
|
import { rtrimSlashes } from '../../path-utils';
|
||||||
import InteropService_Importer_Md from './InteropService_Importer_Md';
|
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 Logger from '@joplin/utils/Logger';
|
||||||
import { uuidgen } from '../../uuid';
|
import { uuidgen } from '../../uuid';
|
||||||
import shim from '../../shim';
|
import shim from '../../shim';
|
||||||
@@ -41,34 +41,67 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
|
|||||||
return normalize(withoutBasePath).split(sep)[0];
|
return normalize(withoutBasePath).split(sep)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async exec(result: ImportExportResult) {
|
private async extractFiles_(sourcePath: string, targetPath: string) {
|
||||||
const sourcePath = rtrimSlashes(this.sourcePath_);
|
const fileExtension = extname(sourcePath).toLowerCase();
|
||||||
const unzipTempDirectory = await this.temporaryDirectory_(true);
|
const fileNameNoExtension = basename(sourcePath, extname(sourcePath));
|
||||||
|
if (fileExtension === '.zip') {
|
||||||
logger.info('Unzipping files...');
|
logger.info('Unzipping files...');
|
||||||
const files = await shim.fsDriver().zipExtract({ source: sourcePath, extractTo: unzipTempDirectory });
|
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 files = await this.extractFiles_(sourcePath, unzipTempDirectory);
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
result.warnings.push('Zip file has no files.');
|
result.warnings.push('Zip file has no files.');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempOutputDirectory = await this.temporaryDirectory_(true);
|
const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].path);
|
||||||
const baseFolder = this.getEntryDirectory(unzipTempDirectory, files[0].entryName);
|
|
||||||
const notebookBaseDir = join(unzipTempDirectory, baseFolder, sep);
|
const notebookBaseDir = join(unzipTempDirectory, baseFolder, sep);
|
||||||
const outputDirectory2 = join(tempOutputDirectory, baseFolder);
|
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');
|
const { oneNoteConverter } = shim.requireDynamic('@joplin/onenote-converter');
|
||||||
|
|
||||||
logger.info('Extracting OneNote to HTML');
|
logger.info('Extracting OneNote to HTML');
|
||||||
const skippedFiles = [];
|
const skippedFiles = [];
|
||||||
for (const notebookFile of notebookFiles) {
|
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
|
// In some cases, the OneNote zip file can include folders and other files
|
||||||
// that shouldn't be imported directly. Skip these:
|
// that shouldn't be imported directly. Skip these:
|
||||||
if (!['.one', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
|
if (!['.one', '.onetoc2'].includes(extname(notebookFilePath).toLowerCase())) {
|
||||||
logger.info('Skipping non-OneNote file:', notebookFile.entryName);
|
logger.info('Skipping non-OneNote file:', notebookFile.path);
|
||||||
skippedFiles.push(notebookFile.entryName);
|
skippedFiles.push(notebookFile.path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,14 +127,23 @@ export default class InteropService_Importer_OneNote extends InteropService_Impo
|
|||||||
...this.options_,
|
...this.options_,
|
||||||
format: 'html',
|
format: 'html',
|
||||||
outputFormat: ImportModuleOutputFormat.Html,
|
outputFormat: ImportModuleOutputFormat.Html,
|
||||||
|
|
||||||
});
|
});
|
||||||
logger.info('Finished');
|
logger.info('Finished');
|
||||||
result = await importer.exec(result);
|
result = await importer.exec(result);
|
||||||
|
|
||||||
return 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) {
|
private async moveSvgToLocalFile(baseFolder: string) {
|
||||||
const htmlFiles = await this.getValidHtmlFiles(resolve(baseFolder));
|
const htmlFiles = await this.getValidHtmlFiles(resolve(baseFolder));
|
||||||
|
|
||||||
|
|||||||
@@ -210,4 +210,5 @@ IDPSSO
|
|||||||
nameid
|
nameid
|
||||||
attrname
|
attrname
|
||||||
dpkg
|
dpkg
|
||||||
|
onepkg
|
||||||
ATTLIST
|
ATTLIST
|
||||||
Reference in New Issue
Block a user