Import a zip database in a self-hostedV5 from the terminal

Hey guys,

I was wondering if it was possible to import my backup, which is a zip file, from the terminal.
Is there a command or a script that I can use ?
I know that I can do it from the GUI using /settings/backup_restore/restore but as I need to automate most of it for a project, I really need to do it from a terminal.

Thank you and great evening !

Hi,

The frontend also uses the API, you can use the network tab in the browser console to debug the API request.

Hi,

Thank you for your reply.
The API request is POST /api/v1/import_json?&import_settings=true&import_data=true. But for security reasons, there are the X-XSRF-TOKEN, laravel_session and the X-Api-Token that are also in the request and that I cannot hardcode.

Any clues how to automate the process ?
I thought at first to login using also the API : POST /api/v1/login?first_load=true&include_static=true, but in the query sent by the client, there is also a XSRF-TOKEN and laravel_session and I have no idea how to generate them.

Thank you :slight_smile:

Here are the API request if they are useful :
Login page :

POST /api/v1/login?first_load=true&include_static=true HTTP/1.1
Host: <IP>
Content-Length: 78
X-Requested-With: XMLHttpRequest
X-API-SECRET: 
X-CLIENT-VERSION: 5.0.156
User-Agent: <user-agent>
Content-Type: application/json; charset=utf-8
Accept: */*
Origin: <IP>
Referer: <IP>
Accept-Encoding: gzip, deflate
Accept-Language: q=0.9,en-US;q=0.8,en;q=0.7
Cookie: XSRF-TOKEN=[REDACTED]; laravel_session=[REDACTED]
Connection: close

{"email":"<my_email>","password":"<my_password>","one_time_password":""}

Restoring backup :

POST /api/v1/import_json?&import_settings=true&import_data=true HTTP/1.1
Host: <IP>
Content-Length: 10037
Accept: application/json, text/plain, */*
X-React: true
X-XSRF-TOKEN: [REDACTED]
X-Requested-With: XMLHttpRequest
X-Api-Token: [REDACTED]
User-Agent: <user-agent>
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryMlUW4HSzLDMUKTT8
Origin: <IP>
Referer: <IP>/settings/backup_restore/restore
Accept-Encoding: gzip, deflate
Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7
Cookie: XSRF-TOKEN=[REDACTED]; laravel_session=[REDACTED]
Connection: close

------WebKitFormBoundaryMlUW4HSzLDMUKTT8
Content-Disposition: form-data; name="files"; filename="invoice_ninja.zip"
Content-Type: application/zip

[CONTENT OF MY FILE]
------WebKitFormBoundaryMlUW4HSzLDMUKTT8--

I believe you only need to provide the API token

Getting the API token is fine but getting to restore the backup does not work. Since this is not a token error, I suppose that this from the headers of the requests.

My code :

import requests

def get_token(email, password):
    url = "<IP>/api/v1/login"
    headers = {
        "Accept": "application/json, text/plain, */*",
        "X-React": "true",
        "X-Requested-With": "XMLHttpRequest",
        "X-Api-Token": "null",
        "User-Agent": "<user-agent>",
        "Content-Type": "application/json",
        "Origin": "<IP>",
        "Referer": "<IP>",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "q=0.9,en-US;q=0.8,en;q=0.7",
        "Connection": "close",
    }
    
    data = '{{"email":"{0}","password":"{1}","one_time_password":"","secret":""}}'.format(email, password)

    response = requests.post(url, headers=headers, data=data)

    try:
        json_response = response.json()
        token_info = json_response['data'][0]['token']
        token = token_info['token']
        print("Token:", token)
        return token
    except Exception as e:
        print(f"Error parsing JSON response: {e}")

    

def post_backup_via_api(token, file_path):
    url = "<IP>/api/v1/import_json?import_settings=true&import_data=true"
    headers = {
        "Accept": "application/json, text/plain, */*",
        "X-React": "true",
        "X-Requested-With": "XMLHttpRequest",
        "X-Api-Token": token,
        "User-Agent": "<user-agent>",
        "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryMlUW4HSzLDMUKTT8",
        "Origin": "<IP>",
        "Referer": "<IP>/settings/backup_restore/restore",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "q=0.9,en-US;q=0.8,en;q=0.7",
    }

    files = {'file': ('invoice_ninja.zip', open('invoice_ninja.zip', 'rb'), 'application/zip')}

    response = requests.post(url, headers=headers, files=files)
    try:
        json_response = response.json()
        print(json_response)
    except Exception as e:
        result = f"Error parsing JSON response: {e}"
        print(result)


def main_invoice_ninja_backup_via_api(email, password, file_path):
    token = get_token(email, password)
    post_backup_via_api(token, file_path)
    

if __name__ == "__main__":
    email = "email@email.com"
    password = "password"
    file_path = "invoice_ninja.zip"
    main_invoice_ninja_backup_via_api(email, password, file_path)

Here is a sample of the response from the API :

{
   "message":"Call to a member function storeAs() on null",
   "exception":"Error",
   "file":"/var/www/app/app/Http/Controllers/ImportJsonController.php",
   "line":62,
   "trace":[
      {
         "file":"/var/www/app/vendor/laravel/framework/src/Illuminate/Routing/Controller.php",
         "line":54,
         "function":"import",
         "class":"App\\Http\\Controllers\\ImportJsonController",
         "type":"->"
      },
      {
         "file":"/var/www/app/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php",
         "line":43,
         "function":"callAction",
         "class":"Illuminate\\Routing\\Controller",
         "type":"->"
      },
      {
         "file":"/var/www/app/vendor/sentry/sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.php",
         "line":21,
         "function":"dispatch",
         "class":"Illuminate\\Routing\\ControllerDispatcher",
         "type":"->"
      }
....

Any insights ?

My guess is the file isn’t being correctly attached to the request, if you remove the file does the error change?

You are right, if I remove completely the file in the request, I still get the same error which is weird.
The error that I get is a 500 :face_with_raised_eyebrow:.

I removed the “Content-Type” header since it was not needed.
The official documentation does not really describe a standard way of how to write the request.
For reference : Invoice Ninja API Spec

I am kinda stuck for now…

I suggest referencing the request made by the web app, if you match the request exactly it should work.

Capturing the request with Burp and replaying it works but making the request with the request lib of python does not.
I am starting to think that the application adds info to the zip file before sending it to the API…

Sub-question : is there a way to start the instance from a backup ? Using the terminal obviously. That would solve the issue…

Any help would be greatly appreciated.

The application uploads the zip as it is.

Maybe you can use mysqldump instead.