diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8e603d1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.github +.gitignore +README.md diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..1fc1659 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,41 @@ +name: Build container image + +on: + push: + branches: + - "*" + tags: + - "*" + +jobs: + + build_and_push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{github.actor}} + password: ${{secrets.GITHUB_TOKEN}} + - name: Set image tag + id: image-tag + run: | + if [[ ${{ github.ref }} == refs/tags/* ]]; then + echo "IMAGE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + elif [[ ${{ github.ref }} == refs/heads/main ]]; then + echo "IMAGE_TAG=latest" >> $GITHUB_OUTPUT + elif [[ ${{ github.ref }} == refs/heads/develop ]]; then + echo "IMAGE_TAG=develop" >> $GITHUB_OUTPUT + else + echo "IMAGE_TAG=${GITHUB_SHA::8}" >> $GITHUB_OUTPUT + fi + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ghcr.io/facorazza/passwords2bitwarden:${{ steps.image-tag.outputs.IMAGE_TAG }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de30 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.13-slim + +# Set the working directory in the container +WORKDIR /app + +# Create an output directory for the converted files +RUN mkdir -p /app/output + +COPY requirements.txt /app + +RUN python -m pip install --no-cache-dir --user --disable-pip-version-check --upgrade pip +RUN python -m pip install --no-cache-dir --user -r requirements.txt + +# Copy the current directory contents into the container at /app +COPY . /app + +# Make sure the main script is executable +RUN chmod +x main.py + +# Default command for running the script, accepts zip file and output directory as arguments +ENTRYPOINT ["python", "./main.py"] +CMD ["/app/archive.zip", "/app/output"] diff --git a/README.md b/README.md index c8c28fb..404fb41 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,12 @@ Vault converter from Nextcloud Passwords to Bitwarden or Vaultwarden. ## Requirements -The script requires at least Python 3.10. +The script requires at least Python 3.10. Otherwise, you can run the script in a container. -If you're using an older version, you can work around it by using the code mentioned [here](https://github.com/facorazza/Passwords2Bitwarden/issues/8) even though I have not tested it. +If you're using an older version, you can work around it by using the code mentioned [here](https://github.com/facorazza/Passwords2Bitwarden/issues/8) even though I have not tested it. ## How to... -Clone the repo locally: - -``` -git clone https://github.com/facorazza/Passwords2Bitwarden.git -cd Passwords2Bitwarden -``` - -### Requirements - -Install Python requirements: - -``` -python -m pip install -r requirements.txt -``` - ### Nextcloud Passwords > :warning: Before exporting your passwords, you must change your Nextcloud language to English. Got to `Settings > Personal info > Language`. You can revert the change once downloaded the archive. @@ -36,11 +21,42 @@ Enter your Nextcloud instance go to `Passwords > More > Backup & Restore`. Selec 3. Run Export 4. Download CSV -## Conversion +### Conversion -To convert the zip archive call the script like so: +There are two options to run the conversion: the first one is using Docker or Podman, the second one is to run the script by directly cloning the repository. + +The former is more robust in terms of dependencies and you don't need to install anything if you already have a container runtime installed. However, it is yet to be tested. Please open an issue if you find problems with this method or if it worked and the instructions were clear enough. + +The latter method requires you to install a few Python dependencies, but it's been tested and is fairly consistent. + +#### Docker +> You can use the automatically built container image on GitHub or you can build your own using the Dockerfile contained in the repository. + +```shell +docker run --name passwords2bitwarden --rm -v .//.zip:/app/archive.zip -v ./passwords2bitwarden:/app/output facorazza/passwords2bitwarden +``` + +You'll find the exported dump under `passwords2bitwarden`. + +#### Bare-Metal + +Clone the repo locally: + +```shell +git clone https://github.com/facorazza/Passwords2Bitwarden.git +cd Passwords2Bitwarden ``` + +Install Python requirements: + +```shell +python -m pip install -r requirements.txt +``` + +To convert the zip archive call the script like so: + +```shell python main.py ~/Downloads/.../path/to/zip/archive.zip ``` diff --git a/main.py b/main.py index ddddafe..a5cbf86 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ import csv import json import zipfile +import os import click @@ -8,7 +9,9 @@ @click.command() @click.argument("zip_filepath", type=click.Path(exists=True)) -def cli(zip_filepath): +@click.argument("output_dir", type=click.Path(exists=False), default=".") +def cli(zip_filepath, output_dir): + # Extract the ZIP archive with zipfile.ZipFile(zip_filepath, "r") as zf: zf.extractall() @@ -16,6 +19,7 @@ def cli(zip_filepath): dump["folders"] = [] dump["items"] = [] + # Process Folders.csv with open("Folders.csv", "r", encoding="utf-8") as f: folder_structure = {} folders = [] @@ -44,6 +48,7 @@ def cli(zip_filepath): dump["folders"].append({"id": folder["id"], "name": path}) folders.pop(index) + # Process Passwords.csv with open("Passwords.csv", "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: @@ -68,10 +73,16 @@ def cli(zip_filepath): "collectionIds": [], }) - with open("dump.json", "w", encoding="utf-8") as f: - f.write(json.dumps(dump)) + # Ensure output directory exists + if not os.path.exists(output_dir): + os.makedirs(output_dir) - print("Done! Upload dump.json to Bitwarden or Vaultwarden.") + # Save to the output directory + output_file = os.path.join(output_dir, "dump.json") + with open(output_file, "w", encoding="utf-8") as f: + f.write(json.dumps(dump, indent=4)) + + print(f"Done! Upload {output_file} to Bitwarden or Vaultwarden.") if __name__ == "__main__": diff --git a/utils.py b/utils.py index c13ac90..2cad59c 100644 --- a/utils.py +++ b/utils.py @@ -11,7 +11,7 @@ def parse_custom_fields(fields): case "secret": field_type = 1 # Hidden field case _: - field_type = 0 # Default to text field + field_type = 0 # Defaults to text field custom_fields.append({ "name": field[0],