Blazor WebAssembly with Cookie Authentication

In the Backend I’ve started by adding Cookie Authentication in the startup and override the OnRedirectToLogin event handlers, so they are going to return a HTTP Status Code 401 to the consumer. This is handled in the Exception Handling Middleware and not shown here.

// Cookie Authentication
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax; // We don't want to deal with CSRF Tokens

        options.Events.OnRedirectToAccessDenied = context => throw new AuthenticationFailedException();
        options.Events.OnRedirectToLogin = context => throw new AuthenticationFailedException();
    });

The user is signed in using HttpContext#SignInAsync with something along the lines of a AuthenticationController:

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

// ...

namespace RebacExperiments.Server.Api.Controllers
{
    public class AuthenticationController : ODataController
    {
        // ...

        [HttpPost("odata/SignInUser")]
        public async Task SignInUser([FromServices] IUserService userService, [FromBody] ODataActionParameters parameters, CancellationToken cancellationToken)
        {
            // ...

            // Create the ClaimsPrincipal
            var claimsIdentity = new ClaimsIdentity(userClaims, CookieAuthenticationDefaults.AuthenticationScheme);
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            // It's a valid ClaimsPrincipal, sign in
            await HttpContext.SignInAsync(claimsPrincipal, new AuthenticationProperties { IsPersistent = rememberMe });
            // ...
        }
    }
}        

You can then open your Browsers Developer Tools and see, that an (encrypted) Cookie has been created.

Once we have successfully logged in and got our Cookie, we need to send the Authorization Cookie on every request to the API. So we start by adding a CookieDelegatingHandler, that does just that:

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components.WebAssembly.Http;
using RebacExperiments.Blazor.Shared.Logging;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class CookieDelegatingHandler : DelegatingHandler
    {
        private readonly ILogger _logger;

        public CookieDelegatingHandler(ILogger logger)
        {
            _logger = logger;
        }

        protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _logger.TraceMethodEntry();

            request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

            return await base.SendAsync(request, cancellationToken);
        }
    }
}

The CookieDelegatingHandler needs to be registered for the HttpClient, so we use the IHttpClientBuilder#AddHttpMessageHandler extension method like this:

builder.Services
    .AddHttpClient(client => client.BaseAddress = new Uri("https://localhost:5000"))
    .AddHttpMessageHandler();

The Blazor Authorization Infrastructure uses an AuthenticationStateProvider to pass the user information into the components. We want to persist the user information across page refreshes, so the local storage of a Browser seems to be a good place to persist it.

We don’t need to take additional dependencies, just write a small LocalStorageService.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.JSInterop;
using System.Text.Json;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class LocalStorageService
    {
        private IJSRuntime _jsRuntime;

        public LocalStorageService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public async Task GetItemAsync(string key)
        {
            var json = await _jsRuntime.InvokeAsync("localStorage.getItem", key);

            if (json == null)
            {
                return default;
            }

            return JsonSerializer.Deserialize(json);
        }

        public async Task SetItem(string key, T value)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
        }

        public async Task RemoveItemAsync(string key)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
        }
    }
}

And register it in the Program.cs.

builder.Services.AddSingleton();

We can then implement an AuthenticationStateProvider, that allows us to set a User (think of User Profile) and notify subscribers about the new AuthenticationState. The User is persisted using our LocalStorageService.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components.Authorization;
using RebacExperiments.Shared.ApiSdk.Models;
using System.Security.Claims;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private const string LocalStorageKey = "currentUser";

        private readonly LocalStorageService _localStorageService;

        public CustomAuthenticationStateProvider(LocalStorageService localStorageService)
        {
            _localStorageService = localStorageService;
        }

        public override async Task GetAuthenticationStateAsync()
        {
            var currentUser = await GetCurrentUserAsync();

            if(currentUser == null)
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }

            Claim[] claims = [
                new Claim(ClaimTypes.NameIdentifier, currentUser.Id!.ToString()!),
                new Claim(ClaimTypes.Name, currentUser.LogonName!.ToString()!),
                new Claim(ClaimTypes.Email, currentUser.LogonName!.ToString()!)
            ];

            var authenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(CustomAuthenticationStateProvider))));

            return authenticationState;
        }

        public async Task SetCurrentUserAsync(User? currentUser)
        { 
            await _localStorageService.SetItem(LocalStorageKey, currentUser);

            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public Task GetCurrentUserAsync() => _localStorageService.GetItemAsync(LocalStorageKey);
    }
}

Don’t forget to register all authentication related services.

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton();
builder.Services.AddSingleton(s => s.GetRequiredService());

In the App.razor add the CascadingAuthenticationState and AuthorizeRouteView components, so the AuthenticationState flows down to the components automagically.

@using Microsoft.AspNetCore.Components.Authorization



    
        
            
        
        
            Not found
            
                

Sorry, there's nothing at this address.

In the MainLayout, you can then use the <AuthorizeView> component, that allows to check, if a given user is authorized or not. If the User is not authorized, we are redirecting to the Login page using a <RedirectToLogin> component.

@using Microsoft.AspNetCore.Components
@using System.Runtime.InteropServices
@using RebacExperiments.Blazor.Components
@using RebacExperiments.Blazor.Components.RedirectToLogin
@namespace RebacExperiments.Blazor.Shared

Relationship-based Experiments with ASP.NET Core OData

    
        
    
    
        
    

The <RedirectToLogin> component simply uses the NavigationManager to navigate to the Login Page.

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        var baseRelativePath = Navigation.ToBaseRelativePath(Navigation.Uri);

        if(string.IsNullOrWhiteSpace(baseRelativePath))
        {
            Navigation.NavigateTo($"Login", true);
        } else {
            Navigation.NavigateTo($"Login?returnUrl={Uri.EscapeDataString(baseRelativePath)}", true);
        }
    }
}

Now what happens, if the Web service returns a HTTP Status Code 401 (Unauthorized) and we still have the User in the Local Storage? Yes, it will be out of sync. So we need to update the AuthenticationState and clear the User information, if the service returns a HTTP Status Code 401.

This can be done by using a DelegatingHandler, that takes a dependency on our CustomAuthenticationStateProvider, and sets the current User to null. This should inform all subscribers, that we are now unauthorized to perform actions.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using RebacExperiments.Blazor.Shared.Logging;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class UnauthorizedDelegatingHandler : DelegatingHandler
    {
        private readonly ILogger _logger;

        private readonly CustomAuthenticationStateProvider _customAuthenticationStateProvider;

        public UnauthorizedDelegatingHandler(ILogger logger, CustomAuthenticationStateProvider customAuthenticationStateProvider)
        {
            _logger = logger;
            _customAuthenticationStateProvider = customAuthenticationStateProvider;
        }

        protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _logger.TraceMethodEntry();

            var response = await base.SendAsync(request, cancellationToken);

            if(response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                var currentUser = await _customAuthenticationStateProvider.GetCurrentUserAsync();

                if(currentUser != null)
                {
                    await _customAuthenticationStateProvider.SetCurrentUserAsync(null);
                }
            }

            return response;
        }
    }
}

You need to add the UnauthorizedDelegatingHandler to the HttpClient.

builder.Services
    .AddHttpClient(client => client.BaseAddress = new Uri("https://localhost:5000"))
    .AddHttpMessageHandler()
    .AddHttpMessageHandler();

Now let’s connect everything!

I want the Login Page to have its own layout and don’t want to use the MainLayout. So I am adding an <EmptyLayout> component.

@inherits LayoutComponentBase

@Body

This EmptyLayout is then used as the Layout for the Login Page, so I can style it to my needs. The example uses a <SimpleValidator> for validation, that has been developed in a previous article. You could easily replace it with a <DataAnnotationsValidator>, to use Blazors built-in validations.

@page "/Login"
@layout EmptyLayout

@using RebacExperiments.Shared.ApiSdk

@inject ApiClient ApiClient
@inject IStringLocalizer Loc
@inject NavigationManager NavigationManager
@inject CustomAuthenticationStateProvider AuthStateProvider

Login

Login @if(!string.IsNullOrWhiteSpace(ErrorMessage)) { }

Let’s take a look at the Login.razor.cs Code-Behind.

The Login#SignInUserAsync methods starts by logging the User in. The Server will return the HttpOnly Cookie, that’s going to be sent with every request to the API. To get the User information for populating the AuthenticationState the /Me endpoint is called. The User is the set in the AuthStateProvider and we navigate to our application.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components;
using RebacExperiments.Shared.ApiSdk.Odata.SignInUser;
using System.ComponentModel.DataAnnotations;
using RebacExperiments.Blazor.Infrastructure;
using Microsoft.Extensions.Localization;

namespace RebacExperiments.Blazor.Pages
{
    public partial class Login
    {
        /// 
        /// Data Model for binding to the Form.
        /// 
        private sealed class InputModel
        {
            /// 
            /// Gets or sets the Email.
            /// 
            [Required]
            [EmailAddress]
            public required string Email { get; set; }

            /// 
            /// Gets or sets the Password.
            /// 
            [Required]
            [DataType(DataType.Password)]
            public required string Password { get; set; }

            /// 
            /// Gets or sets the RememberMe Flag.
            /// 
            [Required]
            public bool RememberMe { get; set; } = false;
        }

        // Default Values.
        private static class Defaults
        {
            public static class Philipp
            {
                public const string Email = "[email protected]";
                public const string Password = "5!F25GbKwU3P";
                public const bool RememberMe = true;
            }

            public static class MaxMustermann
            {
                public const string Email = "[email protected]";
                public const string Password = "5!F25GbKwU3P";
                public const bool RememberMe = true;
            }
        }


        /// 
        /// If a Return URL is given, we will navigate there after login.
        /// 
        [SupplyParameterFromQuery(Name = "returnUrl")]
        private string? ReturnUrl { get; set; }

        /// 
        /// The Model the Form is going to bind to.
        /// 
        [SupplyParameterFromForm]
        private InputModel Input { get; set; } = new()
        {
            Email = Defaults.Philipp.Email,
            Password = Defaults.Philipp.Password,
            RememberMe = Defaults.Philipp.RememberMe
        };

        /// 
        /// Error Message.
        /// 
        private string? ErrorMessage;

        /// 
        /// Signs in the User to the Service using Cookie Authentication.
        /// 
        /// 
        public async Task SignInUserAsync()
        {
            ErrorMessage = null;

            try
            {
                await ApiClient.Odata.SignInUser.PostAsync(new SignInUserPostRequestBody
                {
                    Username = Input.Email,
                    Password = Input.Password,
                    RememberMe = true
                });

                // Now refresh the Authentication State:
                var me = await ApiClient.Odata.Me.GetAsync();

                await AuthStateProvider.SetCurrentUserAsync(me);

                var navigationUrl = GetNavigationUrl();

                NavigationManager.NavigateTo(navigationUrl);
            }
            catch
            {
                ErrorMessage = Loc["Login_Failed"];
            }
        }

        private string GetNavigationUrl()
        {
            if(string.IsNullOrWhiteSpace(ReturnUrl))
            {
                return "/";
            }

            return ReturnUrl;
        }

        /// 
        /// Validates an <see cref="InputModel"/>.
        /// 
        /// InputModel to validate
        /// The list of validation errors for the EditContext model fields
        private IEnumerable ValidateInputModel(InputModel model)
        {
            if(string.IsNullOrWhiteSpace(model.Email))
            {
                yield return new ValidationError
                {
                    PropertyName = nameof(model.Email),
                    ErrorMessage = Loc.GetString("Validation_IsRequired", nameof(model.Email))
                };
            }

            if(string.IsNullOrWhiteSpace(model.Password))
            {
                yield return new ValidationError
                {
                    PropertyName = nameof(model.Password),
                    ErrorMessage = Loc.GetString("Validation_IsRequired", nameof(model.Password))
                };
            }
        }
    }
}

In the Login.razor.css we add a bit of styling.

@keyframes fade {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.container {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    animation: fade 0.2s ease-in-out forwards;
}

h1 {
    font-size: 35px;
    font-weight: 100;
    text-align: center;
}

Conclusion

And that’s it! You will now be able to use Cookie Authentication in your Blazor Application.

References
https://www.bytefish.de/blog/blazor_wasm_cookie_authentication.html

Custom NavLink to Support Complex URL Matching in ASP.NET Blazor

To make the “Home” NavLink selected when navigating to http://localhost:3002/ or http://localhost:3002/Monitoring/, you can adjust the Match attribute of the NavLink to use a custom match condition. Blazor does not support complex URL matching directly out of the box, but you can achieve this by creating a custom CustomNavLink component.

CustomNavLink.razor

@inject NavigationManager Navigation
@implements IDisposable

@if (IsActive)
{
    <NavLink class="nav-link active" href="" Match="NavLinkMatch.All">
        @ChildContent
    </NavLink>
}
else
{
    <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
        @ChildContent
    </NavLink>
}

@code {
    [Parameter] public RenderFragment ChildContent { get; set; } = default!;

    private bool IsActive { get; set; }

    protected override void OnInitialized()
    {
        Navigation.LocationChanged += OnLocationChanged;
    }
    
    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        // Handle the URL change here
        IsActive = MatchUrl();
        StateHasChanged(); // Update the UI
    }
    
    private bool MatchUrl()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri).PathAndQuery;

        if (uri.Equals("/"))
        {
            return true;
        }

        if (uri.StartsWith("/Monitoring", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }
        
        return false;
    }

    [Inject] private NavigationManager NavigationManager { get; set; } = default!;
    public void Dispose()
    {
        Navigation.LocationChanged -= OnLocationChanged;
    }

}
<CustomNavLink>
    <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</CustomNavLink>

 

Handle an Unknown Number of Route Parameters in ASP.NET Blazor

To handle an unknown number of route parameters in ASP.NET Blazor, you can define a single route with a wildcard parameter to capture the entire path.

Here’s how you can achieve this in Blazor:

  1. Define the Route: Use a route parameter in the @page directive. The wildcard parameter is specified using an asterisk (*). This allows you to capture the entire path as a single string.
  2. Process the Parameters: Once you have the full path, you can split it into individual parameters and process them as needed.
@page "/Monitoring/{**Parameters}"
@inject NavigationManager Navigation


<h3>Monitoring</h3>

<ul>
    @foreach (var param in RouteParameters)
    {
    <li>@param</li>
    }
</ul>
[Parameter]
public string Parameters { get; set; }

private List<string> RouteParameters { get; set; } = new();

protected override void OnParametersSet()
{
    var uri = new Uri(Navigation.Uri);
    var path = uri.AbsolutePath;

    // Remove the initial part of the path
    var trimmedPath = path.Substring("/Monitoring/".Length);

    // Split the remaining part of the path by '/'
    RouteParameters = trimmedPath.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
}

 

Update Kernel arguments for better performance in Ubuntu 24.04

Here’s how you can apply these kernel parameters in Ubuntu 24.04:

  1. Edit GRUB Configuration File:

    Open the GRUB configuration file in a text editor. For example, you can use nano:

    sudo nano /etc/default/grub
    
  2. Add Kernel Parameters:

    Find the line that starts with GRUB_CMDLINE_LINUX_DEFAULT and add your parameters to the list. It should look something like this:

    GRUB_CMDLINE_LINUX_DEFAULT="quiet splash mitigations=off mem_sleep_default=s2idle nvidia-drm.modeset=1"
    

    Ensure that all parameters are enclosed within the same set of quotes.

  3. Update GRUB:

    After saving the changes, you need to update the GRUB configuration:

    sudo update-grub
    
  4. Reboot:

    Finally, reboot your system to apply the changes:

    sudo reboot
    

This will apply the desired kernel parameters on all boot entries.

Install Android ADB on Ubuntu Linux

To install Android ADB (Android Debug Bridge) on Ubuntu, you can follow these steps:

Identify the Vendor ID:

Connect your Android device to your computer and run the following command to identify the vendor ID:

lsusb

Look for the line that corresponds to your Android device. The vendor ID is the first part of the ID after ID, for example, 18d1 for Google.

Update Your Package List: Open your terminal and update the package list to ensure you have the latest information on the newest versions of packages and their dependencies.

sudo apt update

Install ADB: You can install the ADB package using the following command:

sudo apt install android-tools-adb

Verify Installation: After installation, you can verify that ADB is installed correctly by checking its version:

adb version

Add Your User to the Plugdev Group (Optional): This step ensures that you can use ADB without root permissions. It’s especially useful when working with devices.

sudo usermod -aG plugdev $USER

Then, log out and log back in to apply the changes.

Set Up Udev Rules (Optional): To communicate with your Android device over USB, you might need to set up udev rules. Create a new udev rules file:

sudo nano /etc/udev/rules.d/51-android.rules

Add the following line to the file, replacing xxxx with your device’s USB vendor ID (you can find a list of these IDs online or in the documentation for your device):

SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"

Save and close the file, then reload the udev rules:

sudo udevadm control --reload-rules

Now, you should have ADB installed and configured on your Ubuntu system. You can connect your Android device and start using ADB commands.

Filter a list of objects by a property in Python

To filter a list of objects by a property in Python, you can use a list comprehension. Here is an example demonstrating how to filter a list of objects by a specific property:

# Define a sample class
class Employee:
    def __init__(self, name, department, salary):
        self.name = name
        self.department = department
        self.salary = salary

# Create a list of Employee objects
employees = [
    Employee("Alice", "HR", 60000),
    Employee("Bob", "IT", 75000),
    Employee("Charlie", "HR", 55000),
    Employee("David", "IT", 80000),
    Employee("Eve", "Finance", 65000)
]

# Filter the list by department
filtered_employees = [employee for employee in employees if employee.department == "HR"]

# Print the filtered list
for employee in filtered_employees:
    print(f"Name: {employee.name}, Department: {employee.department}, Salary: {employee.salary}")

In this example, we define a class Employee and create a list of Employee objects. We then use a list comprehension to filter the list by the department property, keeping only the employees who work in the “HR” department. Finally, we print the details of the filtered employees.

You can modify the filter condition inside the list comprehension to filter by any other property or criteria.

Migrating a Git repository from HTTPS to SSH

Step 1: Verify SSH Keys

First, ensure you have SSH keys set up on your machine and added to your GitHub account.

  1. Check for existing SSH keys:
    ls -al ~/.ssh

    Look for files named id_rsa and id_rsa.pub or similar.

  2. Generate a new SSH key (if necessary):
    ssh-keygen -t rsa -b 4096 -C "[email protected]"
    

    Follow the prompts to save the key (default location is fine).

  3. Add your SSH key to the SSH agent:
    eval "$(ssh-agent -s)"
    ssh-add ~/.ssh/id_rsa
    
  4. Add the SSH key to your GitHub account: Copy the contents of your public key file to the clipboard:
    cat ~/.ssh/id_rsa.pub
    

    Then, add it to your GitHub account by going to Settings > SSH and GPG keys > New SSH key.

Step 2: Update Remote URL

  1. Navigate to your local repository:
    cd /path/to/your/repo
    
  2. Get the current remote URL:
    git remote -v
    
  3. Update the remote URL to use SSH: Replace origin with the name of your remote if it’s different.
    git remote set-url origin [email protected]:username/repo.git
    

    Replace username with your GitHub username and repo with the name of your repository.

  4. Verify the change:
    git remote -v
    

    You should see the SSH URL listed.

Step 3: Test the Connection

  1. Test the SSH connection:

    You should see a success message like “Hi username! You’ve successfully authenticated, but GitHub does not provide shell access.”

  2. Fetch from the remote to ensure everything is working:
    git fetch
    

If everything is set up correctly, your repository should now be using SSH instead of HTTPS.

Install Android ADB on Fedora Linux

sudo dnf install android-tools

Create a plugdev group (if it doesn’t already exist) and add the user to it:

sudo groupadd plugdev
sudo usermod -aG plugdev $LOGNAME

Log out and log back in for the group change to take effect. Use the id command to verify that you are in the plugdev group:

id

Identify the Vendor ID:

Connect your Android device to your computer and run the following command to identify the vendor ID:

lsusb

Look for the line that corresponds to your Android device. The vendor ID is the first part of the ID after ID, for example, 18d1 for Google.

Create a udev Rule:

Create a file for the udev rule:

sudo nano /etc/udev/rules.d/51-android.rules

Add the Following Content:

In the file, add a rule to grant the necessary permissions. Replace YOUR-VENDOR-ID with your device’s vendor ID. Here’s an example rule:

SUBSYSTEM=="usb", ATTR{idVendor}=="YOUR-VENDOR-ID", MODE="0666", GROUP="plugdev"

For example, if the vendor ID is 18d1 (Google):

SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"

Change File Permissions:

Change the permissions of the file to make it readable:

sudo chmod a+r /etc/udev/rules.d/51-android.rules

Reload udev rules:

sudo udevadm control --reload-rules
sudo udevadm trigger

Restart your machine.

Check ADB Devices:

Verify that your device is recognized correctly:

adb devices

References
https://developer.android.com/studio/run/device

Using Data Recovery Tools in Linux

Stop Using the Drive

Immediately stop using the drive to prevent further data overwriting.

Identify the Overwritten Part

Determine how much data the ISO has overwritten. Typically, writing an ISO will overwrite the beginning of the drive, including the partition table and some initial sectors.

Use Data Recovery Tools

You can try using data recovery tools that work well on Linux:

  1. TestDisk: This tool can help recover lost partitions and make non-booting disks bootable again. It can also recover deleted files from FAT, NTFS, and ext2 filesystems.
  2. PhotoRec: This companion tool to TestDisk specializes in recovering lost files, including videos, documents, and archives from hard disks, CD-ROMs, and lost pictures from memory cards.
  3. ddrescue: A data recovery tool specifically designed to recover data from damaged disks. It can create a disk image and then work on the image to recover data.

Steps to Recover Data

  1. Install Recovery Tools:
    sudo apt-get install testdisk photorec gddrescue
    
  2. Create a Disk Image: It’s safer to work on a disk image rather than directly on the hard drive.
    sudo ddrescue /dev/sdX /path/to/image.img /path/to/logfile.log
    

    Replace /dev/sdX with your external hard drive’s identifier.

  3. Run TestDisk:
    sudo testdisk /path/to/image.img
    

    Follow the on-screen instructions to analyze and recover lost partitions.

  4. Run PhotoRec:
    sudo photorec /path/to/image.img
    

    This will guide you through recovering individual files.

upsert (insert or update) operation using SQLAlchemy

To achieve an “upsert” (insert or update) operation using SQLAlchemy, you can use the merge method. This method will check if the record exists and update it if it does, or insert it if it does not. Here’s how you can do it:

  1. Define your SQLAlchemy model: Ensure that you have your model defined. For example, let’s assume you have a model called MyModel.
    from sqlalchemy import Column, Integer, String, create_engine
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker
    
    Base = declarative_base()
    
    class MyModel(Base):
        __tablename__ = 'my_model'
        id = Column(Integer, primary_key=True)
        name = Column(String)
        value = Column(String)
    
    engine = create_engine('sqlite:///mydatabase.db')
    Session = sessionmaker(bind=engine)
    session = Session()
    
  2. Use the merge method: The merge method will ensure that if the object with the primary key already exists in the database, it will be updated with the new values. If it does not exist, a new record will be created.
    new_data = MyModel(id=1, name='example', value='updated_value')
    session.merge(new_data)
    session.commit()
    

    In this example, if a record with id=1 exists, it will be updated with the new values. If it doesn’t exist, a new record will be created.