diff --git a/.env.example b/.env.example
index d2804fc..c415c49 100644
--- a/.env.example
+++ b/.env.example
@@ -13,3 +13,6 @@ KEEPASS_BACKUP_PATH="./backup"
KEEPASS_BACKUP_FILE_NAME="BitwardenBackup" # same name every time: overwrite
KEEPASS_BACKUP_DATABASE_NAME="BitwardenBackup_%date%"
ORGANIZATIONS_GROUP_NAME="Organizations"
+ORGANIZATION_MODE="flat"
+ORGANIZATION_FOLDERS_NAME="Folders"
+ORGANIZATION_COLLECTIONS_NAME="Collections"
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..49eab83
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,39 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## 1.2.0
+
+### Changed
+
+- Organization items are saved differently in the keepass file.
+ - Organizations no longer have each collection as direct keepass group children (and no folders; see fix below).
+ - Organizations now have a keepass group for folders and collections; in these groups the folders and collections are created as keepass groups.
+ - This resembles the structure how it is presented in vaultwarden.
+
+### Added
+
+- Option to save organization items nested with subgroups for folders/collections or flat without duplicate entries.
+
+### Fixed
+
+- Organization items appearing in the folders outside of the organization.
+
+## 1.1.0
+
+### Added
+
+- Support for keepass key files (thanks to [rahul-kurup](https://github.com/rahul-kurup)).
+
+## 1.0.0
+
+### Added
+
+- Initial implementation.
+
+### Fixed
+
+- Issue where an already valid otpauth://totp/ URI would be incorrectly re-encoded, leading to a corrupted TOTP value (thanks to [DmitriiPetukhov](https://github.com/DmitriiPetukhov)).
diff --git a/README.md b/README.md
index c8f9dda..238cfac 100644
--- a/README.md
+++ b/README.md
@@ -31,21 +31,25 @@ This is similar to projects like [lazywarden](https://github.com/querylab/lazywa
Use the following environment variables to configure the script:
-| variable | default | mandatory | notes |
-| --------------------------------------- | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------- |
-| `URL` | - | x | use the url to your bitwarden/vaultwarden instance |
-| `BW_CLIENTID` | - | x | see [personal api key](https://bitwarden.com/help/personal-api-key/) |
-| `BW_CLIENTSECRET` | - | x | see [personal api key](https://bitwarden.com/help/personal-api-key/) |
-| `BW_PASSWORD` \* | - | x | password to your bitwarden/vaultwarden account (base64-encoded) |
-| `KEEPASS_BACKUP_PASSWORD` \* | _[same as BW_PASSWORD]_ | | password for the KeePass database (base64-encoded) |
-| `KEEPASS_BACKUP_KEYFILE_PATH` | _undefined_ | | provide the key file’s absolute path to use it instead of a password or as an additional security layer. |
-| `ATTACHMENT_TEMP_FOLDER` | ./attachmentBackup | | directory where attachments are temporarily stored (recommendation: use `/tmp` for linux machines) |
-| `MAX_ATTACHMENT_BYTES` | 100000 | | maximum size of an attachment that should be backed up in the KeePass database |
-| `KEEPASS_BACKUP_PATH` | ./backup | | location where KeePass backup should be saved |
-| `KEEPASS_BACKUP_FILE_NAME` | `BitwardenBackup_%date%` | | name of the KeePass database file; use `%date%` anywhere to insert path-friendly date+time string |
-| `KEEPASS_BACKUP_DATABASE_NAME` | _[same as filename]_ | | name of the KeePass database (when opened); can use `%date%` as well |
-| `ORGANIZATIONS_GROUP_NAME` | Organizations | | name of the KeePass group where organizations and its items should be stored |
+| variable | default | mandatory | notes |
+| --------------------------------------- | ------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
+| `URL` | - | x | use the url to your bitwarden/vaultwarden instance |
+| `BW_CLIENTID` | - | x | see [personal api key](https://bitwarden.com/help/personal-api-key/) |
+| `BW_CLIENTSECRET` | - | x | see [personal api key](https://bitwarden.com/help/personal-api-key/) |
+| `BW_PASSWORD` \* | - | x | password to your bitwarden/vaultwarden account (base64-encoded) |
+| `KEEPASS_BACKUP_PASSWORD` \* | _[same as BW_PASSWORD]_ | | password for the KeePass database (base64-encoded); if this not set and keyfile is provided it is not set instead of taking the default value |
+| `KEEPASS_BACKUP_KEYFILE_PATH` | _undefined_ | | absolute path of a key file to use instead of or as an additional security layer to a password |
+| `ATTACHMENT_TEMP_FOLDER` | ./attachmentBackup | | directory where attachments are temporarily stored (recommendation: use `/tmp` for linux machines) |
+| `MAX_ATTACHMENT_BYTES` | 100000 | | maximum size of an attachment that should be backed up in the KeePass database |
+| `KEEPASS_BACKUP_PATH` | ./backup | | location where KeePass backup should be saved |
+| `KEEPASS_BACKUP_FILE_NAME` | `BitwardenBackup_%date%` | | name of the KeePass database file; use `%date%` anywhere to insert path-friendly date+time string |
+| `KEEPASS_BACKUP_DATABASE_NAME` | _[same as filename]_ | | name of the KeePass database (when opened); can use `%date%` as well |
+| `ORGANIZATIONS_GROUP_NAME` | Organizations | | name of the KeePass group where organizations and its items should be stored |
+| `ORGANIZATION_MODE` | flat | | how entries are saved in an organization (flat/nested)\*\* |
+| `ORGANIZATION_FOLDERS_NAME` | Folders | | name of the KeePass group where folders for an organization should be stored; only relevant when mode is nested |
+| `ORGANIZATION_COLLECTIONS_NAME` | Collections | | name of the KeePass group where collections for an organizations should be stored; only relevant when mode is nested |
\*: In most cases these environment variables are stored in plain text. That means they can easily be read. To make this _somewhat_ more secure and conceal them on first sight, your passwords have to be base64-encoded. To encode your password in base64 use some (online) tool of your choice or just open the developer tools console in any browser (usually via F12) and use the output of `btoa("your_password")`.
+\*\*: In flat-mode, all organization entries are saved in the organization itself without creating keepass groups for folders/collections (the folder/collections information is instead saved to a field). Nested-mode creates keepass groups for folders/collections and creates duplicate entries to sort into these groups. The advantage of flat-mode is, that there are no duplicate entries, so entries are better found by searching. The advantage of nested-mode is, that it more closely resembles the structure of the items in vaultwarden, so entries are better found by browsing. Decide yourself, what's more important for your backup strategy.
Depending how you use this script (preferably in your local network), you may access your self-hosted vaultwarden/bitwarden server with a self signed certificate. In this case just set the node environment variable which disables certificate checking: `NODE_TLS_REJECT_UNAUTHORIZED=0`.
diff --git a/docker-compose.yml b/docker-compose.yml
index b87f0db..56f8f5e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,22 +4,25 @@ services:
build: https://github.com/genericFJS/vaultwarden2keepass.git
command: npm run start
volumes:
- - './backup:/app/backup'
+ - "./backup:/app/backup"
# Cache bitwarden cli config (else every start of the script would trigger a "new device" mail)
- - './backup/cli-config:/root/.config/Bitwarden CLI/'
+ - "./backup/cli-config:/root/.config/Bitwarden CLI/"
environment:
# Mandatory
- - 'URL=https://vault.bitwarden.com/'
- - 'BW_CLIENTID=user.aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
- - 'BW_CLIENTSECRET=abcdefghijklmnopqrstuvwxz01234'
- - 'BW_PASSWORD=Y29ycmVjdGhvcnNlYmF0dGVyeXN0YXBsZQ=='
+ - "URL=https://vault.bitwarden.com/"
+ - "BW_CLIENTID=user.aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+ - "BW_CLIENTSECRET=abcdefghijklmnopqrstuvwxz01234"
+ - "BW_PASSWORD=Y29ycmVjdGhvcnNlYmF0dGVyeXN0YXBsZQ=="
# Optional
- - 'KEEPASS_BACKUP_PASSWORD=VHIwdWI0ZG9yJjM='
- - 'KEEPASS_BACKUP_KEYFILE_PATH=/path/to/keyfile.keyx'
- - 'ATTACHMENT_TEMP_FOLDER=/tmp'
- - 'MAX_ATTACHMENT_BYTES=100000'
- - 'KEEPASS_BACKUP_PATH=./backup'
- - 'KEEPASS_BACKUP_FILE_NAME=BitwardenBackup'
- - 'KEEPASS_BACKUP_DATABASE_NAME=BitwardenBackup_%date%'
- - 'ORGANIZATIONS_GROUP_NAME=Organizations'
+ - "KEEPASS_BACKUP_PASSWORD=VHIwdWI0ZG9yJjM="
+ - "KEEPASS_BACKUP_KEYFILE_PATH=/path/to/keyfile.keyx"
+ - "ATTACHMENT_TEMP_FOLDER=/tmp"
+ - "MAX_ATTACHMENT_BYTES=100000"
+ - "KEEPASS_BACKUP_PATH=./backup"
+ - "KEEPASS_BACKUP_FILE_NAME=BitwardenBackup"
+ - "KEEPASS_BACKUP_DATABASE_NAME=BitwardenBackup_%date%"
+ - "ORGANIZATIONS_GROUP_NAME=Organizations"
+ - "ORGANIZATION_MODE=flat"
+ - "ORGANIZATION_FOLDERS_NAME=Folders"
+ - "ORGANIZATION_COLLECTIONS_NAME=Collections"
diff --git a/package.json b/package.json
index 2e59684..d3e3896 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vaultwarden2keepass",
- "version": "1.1.0",
+ "version": "1.2.0",
"type": "module",
"scripts": {
"start": "tsx ./src/index.ts",
diff --git a/src/executeBackup.ts b/src/executeBackup.ts
index 7ef27b5..613ea01 100644
--- a/src/executeBackup.ts
+++ b/src/executeBackup.ts
@@ -8,7 +8,10 @@ const DEFAULTS = {
KEEPASS_BACKUP_PATH: './backup',
KEEPASS_BACKUP_FILE_NAME: 'BitwardenBackup_%date%',
ORGANIZATIONS_GROUP_NAME: 'Organizations',
-};
+ ORGANIZATION_MODE: 'flat',
+ ORGANIZATION_FOLDERS_NAME: 'Folders',
+ ORGANIZATION_COLLECTIONS_NAME: 'Collections',
+} as const;
function ensureParameter(parameter: string, explanation: string) {
const value = process.env[parameter];
@@ -58,9 +61,18 @@ export async function executeBackup() {
process.env['KEEPASS_BACKUP_FILE_NAME'] || DEFAULTS.KEEPASS_BACKUP_FILE_NAME;
const backupDatabaseName = process.env['KEEPASS_BACKUP_DATABASE_NAME'] || backupFileName;
- const organizationsFolderName =
+ const organizationsRootName =
process.env['ORGANIZATIONS_GROUP_NAME'] || DEFAULTS.ORGANIZATIONS_GROUP_NAME;
+ const organizationMode =
+ (process.env['ORGANIZATION_MODE'] || DEFAULTS.ORGANIZATION_MODE).toLowerCase() === 'nested'
+ ? 'nested'
+ : 'flat';
+ const organizationsFoldersName =
+ process.env['ORGANIZATION_FOLDERS_NAME'] || DEFAULTS.ORGANIZATION_FOLDERS_NAME;
+ const organizationsCollectionsName =
+ process.env['ORGANIZATION_COLLECTIONS_NAME'] || DEFAULTS.ORGANIZATION_COLLECTIONS_NAME;
+
// ================ DO BACKUP ================
const backupDate = new Date().toISOString().slice(0, -5).replaceAll(':', '-');
const keepassFileName = backupFileName.replaceAll('%date%', backupDate);
@@ -68,15 +80,19 @@ export async function executeBackup() {
const bitwardenExtractor = new BitwardenExtractor(url, attachmentTempFolder, maxAttachmentBytes);
const bitwardenData = await bitwardenExtractor.getBitwardenData();
+ bitwardenExtractor.logout();
const keepassWriter = new KeePassWriter({
name: keepassDatabaseName,
keyFile: backupKeyFile,
password: backupPassword,
- organizationsFolderName,
+ organizationMode,
+ organizationFolderNames: {
+ organizations: organizationsRootName,
+ collections: organizationsCollectionsName,
+ folders: organizationsFoldersName,
+ },
});
await keepassWriter.fillDatabaseWithBitwardenData(bitwardenData);
await keepassWriter.writeDatabase(backupPath, keepassFileName);
-
- bitwardenExtractor.logout();
}
diff --git a/src/index.ts b/src/index.ts
index e816a91..79bbfed 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,10 +1,5 @@
import { executeBackup } from './executeBackup';
-function success() {
- console.log('🎉 Backup completed successfully');
- process.exit(0);
-}
-
executeBackup().then(
() => {
console.log('🎉 Backup completed successfully');
diff --git a/src/keepassWriter.ts b/src/keepassWriter.ts
index 132ba91..b0ed8ec 100644
--- a/src/keepassWriter.ts
+++ b/src/keepassWriter.ts
@@ -1,5 +1,8 @@
+import { mkdirSync, writeFileSync } from 'fs';
+import type { KdbxEntry } from 'kdbxweb';
+import { join, resolve } from 'path';
+import type { Item, Organization } from './bitwardenCliTypes';
import type { BitwardenData } from './bitwardenExtractor';
-import type { Collection, Folder, Item } from './bitwardenCliTypes';
import { fieldMappings } from './fieldMappings';
import {
getMapping,
@@ -9,24 +12,40 @@ import {
type MappingRecord,
} from './fieldMappingsUtil';
import { Credentials, Kdbx, ProtectedValue, type KdbxGroup } from './kdbxweb';
-import { mkdirSync, writeFileSync } from 'fs';
-import type { KdbxEntry } from 'kdbxweb';
-import { join, resolve } from 'path';
+
+type OrganizationMode = 'flat' | 'nested';
+
+type OrganizationFolderNames = {
+ organizations: string;
+ folders: string;
+ collections: string;
+};
type KeePassWriterArgs = {
name: string;
password?: string;
keyFile?: Uint8Array;
- organizationsFolderName: string;
+ organizationMode: OrganizationMode;
+ organizationFolderNames: OrganizationFolderNames;
};
export class KeePassWriter {
private db: Kdbx;
- private organizationsFolderName!: string;
-
- constructor({ name, password, keyFile, organizationsFolderName }: KeePassWriterArgs) {
+ private organizationMode: OrganizationMode;
+ private organizationFolderNames: OrganizationFolderNames;
+ private allGroupPaths!: string[];
+ private readonly groups: Record = {};
+
+ constructor({
+ name,
+ password,
+ keyFile,
+ organizationMode,
+ organizationFolderNames,
+ }: KeePassWriterArgs) {
const credentials = this.createKbdxCredentials(password, keyFile);
- this.organizationsFolderName = organizationsFolderName;
+ this.organizationMode = organizationMode;
+ this.organizationFolderNames = organizationFolderNames;
this.db = Kdbx.create(credentials, name);
this.db.createDefaultGroup();
}
@@ -75,53 +94,117 @@ export class KeePassWriter {
async fillDatabaseWithBitwardenData(bitwarden: BitwardenData) {
console.log('💻 Filling KeePass database with bitwarden data');
// Transform organizations folder name so there are no collisions with existing folders
- const allRootFolders = new Set([
- ...bitwarden.folders.map((f) => KeePassWriter.getSubfolders(bitwarden.folders, f.name)[0]),
- ...bitwarden.collections.map(
- (c) => KeePassWriter.getSubfolders(bitwarden.collections, c.name)[0],
+ const allRootFolders = new Set(
+ bitwarden.folders.map(
+ (f) =>
+ KeePassWriter.getSubfolders(
+ bitwarden.folders.map((f) => f.name),
+ f.name,
+ )[0],
),
- ]);
- const originalOrganizationsFolder = this.organizationsFolderName;
- if (allRootFolders.has(this.organizationsFolderName)) {
- this.organizationsFolderName = `Bitwarden${originalOrganizationsFolder}`;
+ );
+ const originalOrganizationsFolder = this.organizationFolderNames.organizations;
+ if (allRootFolders.has(originalOrganizationsFolder)) {
+ this.organizationFolderNames.organizations = `Bitwarden${originalOrganizationsFolder}`;
}
- for (let i = 0; allRootFolders.has(this.organizationsFolderName); i++) {
- this.organizationsFolderName = `${originalOrganizationsFolder}_${i}`;
+ for (let i = 0; allRootFolders.has(this.organizationFolderNames.organizations); i++) {
+ this.organizationFolderNames.organizations = `${originalOrganizationsFolder}_${i}`;
}
+ // folders, collections and organizations by id for easy access
+ const pathsById = [...bitwarden.folders, ...bitwarden.collections].reduce<
+ Record
+ >((partial, { id, name }) => {
+ if (id != undefined) partial[id] = name;
+ return partial;
+ }, {});
+ const noFolderName = bitwarden.folders.filter((f) => !f.id)![0].name;
+ const organizations = bitwarden.organizations.reduce>(
+ (partial, o) => ({ ...partial, [o.id]: o }),
+ {},
+ );
+
console.log('⏱️ Adding folders and collections as groups');
- // add folders
- let groups = this.addGroups(this.db.getDefaultGroup(), bitwarden.folders);
-
- // add organizations/collections
- if (bitwarden.organizations.length > 0) {
- const organizationsGroup = this.db.createGroup(
- this.db.getDefaultGroup(),
- this.organizationsFolderName,
- );
- for (const organization of bitwarden.organizations) {
- const organizationGroup = this.db.createGroup(organizationsGroup, organization.name);
- const collections = bitwarden.collections.filter(
- (c) => c.organizationId === organization.id,
- );
- if (collections.length > 0) {
- groups = { ...groups, ...this.addGroups(organizationGroup, collections) };
+ // collect all group paths first, so that the group order is dictated by the group name instead of the item name
+ const groupPaths = new Set();
+ for (const item of bitwarden.items) {
+ if (!item.organizationId) {
+ if (item.folderId) groupPaths.add(pathsById[item.folderId]);
+ } else {
+ groupPaths.add(this.organizationFolderNames.organizations);
+ groupPaths.add(this.groupPathForOrganization(organizations[item.organizationId]));
+ if (this.organizationMode === 'nested') {
+ groupPaths.add(
+ this.groupPathForOrganization(organizations[item.organizationId], 'folder'),
+ );
+ groupPaths.add(
+ this.groupPathForOrganization(
+ organizations[item.organizationId],
+ 'folder',
+ item.folderId ? pathsById[item.folderId] : noFolderName,
+ ),
+ );
+ for (const collectionId of item.collectionIds) {
+ groupPaths.add(
+ this.groupPathForOrganization(organizations[item.organizationId], 'collection'),
+ );
+ groupPaths.add(
+ this.groupPathForOrganization(
+ organizations[item.organizationId],
+ 'collection',
+ pathsById[collectionId],
+ ),
+ );
+ }
}
}
}
+ this.allGroupPaths = [...groupPaths];
+
+ for (const groupPath of [...groupPaths].toSorted((a, b) =>
+ a === this.organizationFolderNames.organizations ? 1 : a.localeCompare(b),
+ )) {
+ this.createGroupRecursive(groupPath);
+ }
+
console.log('⏱️ Adding items as entries');
- const noFolder = bitwarden.folders.find((f) => !f.id);
// add items
for (const item of bitwarden.items) {
// get all group and all collections of item
- const groupsOfItem =
- /* implicitly no folder/collection */ (!item.folderId && item.collectionIds.length === 0) ||
- /* explicitly no folder */ item.folderId === noFolder?.id
- ? [this.db.getDefaultGroup()]
- : [item.folderId, ...item.collectionIds]
- .filter((v) => v !== undefined)
- .map((id) => groups[id]);
+ const groupsOfItem: KdbxGroup[] = [];
+ if (!item.organizationId) {
+ groupsOfItem.push(
+ item.folderId ? this.groups[pathsById[item.folderId]] : this.db.getDefaultGroup(),
+ );
+ } else {
+ if (this.organizationMode === 'nested') {
+ groupsOfItem.push(
+ this.groups[
+ this.groupPathForOrganization(
+ organizations[item.organizationId],
+ 'folder',
+ item.folderId ? pathsById[item.folderId] : noFolderName,
+ )
+ ],
+ );
+ for (const collectionId of item.collectionIds) {
+ groupsOfItem.push(
+ this.groups[
+ this.groupPathForOrganization(
+ organizations[item.organizationId],
+ 'collection',
+ pathsById[collectionId],
+ )
+ ],
+ );
+ }
+ } else {
+ groupsOfItem.push(
+ this.groups[this.groupPathForOrganization(organizations[item.organizationId])],
+ );
+ }
+ }
// Add entry to each group
for (const group of groupsOfItem) {
@@ -178,6 +261,19 @@ export class KeePassWriter {
this.addFields(entry, item.secureNote, item, fieldMappings.secureNote);
}
+
+ // folder/collection for flat organization mode
+ if (item.organizationId && this.organizationMode === 'flat') {
+ if (item.folderId) {
+ entry.fields.set('Folder', pathsById[item.folderId]);
+ }
+ for (const [index, collectionId] of item.collectionIds.entries()) {
+ entry.fields.set(
+ 'Collection' + (item.collectionIds.length === 1 ? '' : `_${index}`),
+ pathsById[collectionId],
+ );
+ }
+ }
}
}
@@ -226,10 +322,10 @@ export class KeePassWriter {
* @param folderOrPath Folder name to find subfolders of.
* @returns Subfolders for folder name.
*/
- static getSubfolders(folders: (Folder | Collection)[], folderOrPath: string) {
+ static getSubfolders(folders: string[], folderOrPath: string) {
const subfolders: string[] = [];
- const allFolders = new Set(folders.map((f) => f.name));
+ const allFolders = new Set(folders);
const potentialSubfolders = folderOrPath.split('/');
const path: string[] = [];
@@ -248,35 +344,43 @@ export class KeePassWriter {
}
/**
- * Adds folders/groups to KeePass database.
+ * Assembles a path for an organization, its folder or collection.
*
- * @param root Root KeePass-group to add (sub-)groups to.
- * @param folders Bitwarden folders to add as groups.
- * @returns Record of bitwarden folder ids with corresponding KeePass groups.
+ * @param organization The organization the path belongs to.
+ * @param path The path itself.
+ * @param type If the path is a folder or a collection.
+ * @returns Complete path as it should be added as a group.
*/
- private addGroups(root: KdbxGroup, folders?: (Folder | Collection)[]): Record {
- if (!folders) return {};
- return folders.reduce(
- (partial, { id, name }) => {
- if (id == null) return partial;
- const subfolders = KeePassWriter.getSubfolders(folders, name);
-
- let parent = root;
- for (const folder of subfolders) {
- const availableParent = [...parent.allGroups()].find((g) => g.name === folder);
- if (!availableParent) {
- parent = this.db.createGroup(parent, folder);
- } else {
- parent = availableParent;
- }
- }
- partial[id] = parent;
- return partial;
- },
- {} as Record,
+ private groupPathForOrganization(
+ organization: Organization,
+ type?: 'folder' | 'collection',
+ path?: string,
+ ) {
+ return (
+ `${this.organizationFolderNames.organizations}/${organization.name}` +
+ (type ? `/${this.organizationFolderNames[`${type}s`]}` + (path ? `/${path}` : '') : '')
);
}
+ /**
+ * Creates a Keepass group based on a path. If the path contains subfolders, they are created recursively.
+ *
+ * @param path Path to create a group for.
+ * @returns The created group.
+ */
+ private createGroupRecursive(path: string): KdbxGroup {
+ const subfolders = KeePassWriter.getSubfolders(this.allGroupPaths, path);
+ const folder = subfolders.at(-1)!;
+ const parent =
+ subfolders.length === 1
+ ? this.db.getDefaultGroup()
+ : this.createGroupRecursive(subfolders.slice(0, -1).join('/'));
+
+ if (!(path in this.groups)) this.groups[path] = this.db.createGroup(parent, folder);
+
+ return this.groups[path];
+ }
+
/**
* Adds a field to the KeePass database based on the Bitwarden field value and its mapping.
*
diff --git a/tests/keepassWriter.spec.ts b/tests/keepassWriter.spec.ts
index 285e69e..c752c26 100644
--- a/tests/keepassWriter.spec.ts
+++ b/tests/keepassWriter.spec.ts
@@ -1,88 +1,34 @@
-import type { Folder } from '../src/bitwardenCliTypes';
+import { describe, expect, it } from 'vitest';
import { KeePassWriter } from '../src/keepassWriter';
-import { describe, it, expect } from 'vitest';
describe('should transform bitwarden folder names to subfolders', () => {
it('gets folders where first folder contains slash', () => {
- const folders: Folder[] = [
- {
- object: 'folder',
- name: 'a/b',
- },
- {
- object: 'folder',
- name: 'a/b/c',
- },
- {
- object: 'folder',
- name: 'a/b/c/d',
- },
- ];
+ const folders: string[] = ['a/b', 'a/b/c', 'a/b/c/d'];
- expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!.name)).toEqual(['a/b', 'c', 'd']);
+ expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!)).toEqual(['a/b', 'c', 'd']);
});
it('gets folders where middle folder contains slash', () => {
- const folders: Folder[] = [
- {
- object: 'folder',
- name: 'a',
- },
- {
- object: 'folder',
- name: 'a/b/c',
- },
- {
- object: 'folder',
- name: 'a/b/c/d',
- },
- ];
+ const folders: string[] = ['a', 'a/b/c', 'a/b/c/d'];
- expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!.name)).toEqual(['a', 'b/c', 'd']);
+ expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!)).toEqual(['a', 'b/c', 'd']);
});
it('gets folders where last folder contains slash', () => {
- const folders: Folder[] = [
- {
- object: 'folder',
- name: 'a',
- },
- {
- object: 'folder',
- name: 'a/b',
- },
- {
- object: 'folder',
- name: 'a/b/c/d',
- },
- ];
+ const folders: string[] = ['a', 'a/b', 'a/b/c/d'];
- expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!.name)).toEqual(['a', 'b', 'c/d']);
+ expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!)).toEqual(['a', 'b', 'c/d']);
});
it('gets folders where first and last folder contains slash', () => {
- const folders: Folder[] = [
- {
- object: 'folder',
- name: 'a/b',
- },
- {
- object: 'folder',
- name: 'a/b/c/d',
- },
- ];
+ const folders: string[] = ['a/b', 'a/b/c/d'];
- expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!.name)).toEqual(['a/b', 'c/d']);
+ expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!)).toEqual(['a/b', 'c/d']);
});
it('gets folders where entire folder contains slash', () => {
- const folders: Folder[] = [
- {
- object: 'folder',
- name: 'a/b/c/d',
- },
- ];
+ const folders: string[] = ['a/b/c/d'];
- expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!.name)).toEqual(['a/b/c/d']);
+ expect(KeePassWriter.getSubfolders(folders, folders.at(-1)!)).toEqual(['a/b/c/d']);
});
});