🏡 index : quine-global-labs/mirror-cockpit-project-cockpit.git

/*
 * Copyright (C) 2023 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from "cockpit";
import React from "react";
import client from "../client";

import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { CardBody } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { DescriptionList } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";

import { StorageButton, StorageLink } from "../storage-controls.jsx";
import {
    dialog_open,
    SelectOne, TextInput,
    init_teardown_usage, BlockingMessage, TeardownMessage
} from "../dialog.jsx";
import { block_name, fmt_size, get_active_usage, teardown_active_usage, reload_systemd } from "../utils.js";
import { check_unused_space, get_resize_info, free_space_after_part, grow_dialog, shrink_dialog } from "../block/resize.jsx";
import { StorageCard, StorageDescription, new_card, navigate_away_from_card } from "../pages.jsx";

const _ = cockpit.gettext;

export function delete_partition(block, card) {
    const block_part = client.blocks_part[block.path];
    const name = block_name(block);
    const usage = get_active_usage(client, block.path, _("delete"));

    if (usage.Blocking) {
        dialog_open({
            Title: cockpit.format(_("$0 is in use"), name),
            Body: BlockingMessage(usage)
        });
        return;
    }

    dialog_open({
        Title: cockpit.format(_("Permanently delete $0?"), name),
        Teardown: TeardownMessage(usage),
        Action: {
            Danger: _("Deleting a partition will delete all data in it."),
            Title: _("Delete"),
            action: async function () {
                await teardown_active_usage(client, usage);
                await block_part.Delete({ 'tear-down': { t: 'b', v: true } });
                await reload_systemd();
                navigate_away_from_card(card);
            }
        },
        Inits: [
            init_teardown_usage(client, usage)
        ]
    });
}

export function make_partition_card(next, block) {
    const block_part = client.blocks_part[block.path];
    const unused_space_warning = check_unused_space(block.path);
    const unused_space = !!unused_space_warning;
    let { info, shrink_excuse, grow_excuse } = get_resize_info(client, block, unused_space);

    if (!unused_space_warning && !grow_excuse && free_space_after_part(client, block_part) === 0) {
        grow_excuse = _("No free space after this partition");
    }

    const type = block_part.Type.replace(/^0x/, "").toLowerCase();
    let type_extra;
    if (type === "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" || type === "ef")
        type_extra = _("EFI system partition");
    else if (type === "21686148-6449-6e6f-744e-656564454649")
        type_extra = _("BIOS boot partition");

    const card = new_card({
        title: _("Partition"),
        type_extra,
        next,
        page_block: block,
        for_summary: true,
        has_warning: !!unused_space_warning,
        component: PartitionCard,
        props: { block, unused_space_warning, resize_info: info },
        actions: [
            (!unused_space &&
             {
                 title: _("Shrink"),
                 action: () => shrink_dialog(client, block_part, info),
                 excuse: shrink_excuse,
             }),
            (!unused_space &&
             {
                 title: _("Grow"),
                 action: () => grow_dialog(client, block_part, info),
                 excuse: grow_excuse,
             }),
            {
                title: _("Delete"),
                action: () => delete_partition(block, card),
                danger: true,
            },
        ],
    });
    return card;
}

const gpt_type_names = {
    "21686148-6449-6e6f-744e-656564454649": _("BIOS boot partition"),
    "c12a7328-f81f-11d2-ba4b-00a0c93ec93b": _("EFI system partition"),
    "9e1a2d38-c612-4316-aa26-8b49521e5a8b": _("PowerPC PReP boot partition"),
    "0657fd6d-a4ab-43c4-84e5-0933c84b4f4f": _("Linux swap space"),
    "0fc63daf-8483-4772-8e79-3d69d8477de4": _("Linux filesystem data"),
    "e6d6d379-f507-44c2-a23c-238f2a3df928": _("Logical Volume Manager partition"),
};

const mbr_type_names = {
    ef: _("EFI system partition"),
    82: _("Linux swap space"),
    83: _("Linux filesystem data"),
    "8e": _("Logical Volume Manager partition"),
};

const PartitionCard = ({ card, block, unused_space_warning, resize_info }) => {
    const block_part = client.blocks_part[block.path];
    const unused_space = !!unused_space_warning;
    const block_ptable = client.blocks_ptable[block_part.Table];
    const type_names = block_ptable?.Type === "dos" ? mbr_type_names : gpt_type_names;
    const type = block_part.Type.replace(/^0x/, "").toLowerCase();

    function shrink_to_fit() {
        return shrink_dialog(client, block_part, resize_info, true);
    }

    function grow_to_fit() {
        return grow_dialog(client, block_part, resize_info, true);
    }

    function set_type_dialog() {
        const choices = Object.keys(type_names).map(k => ({ value: k, title: type_names[k] }));
        choices.push({ title: _("Custom"), value: "custom" });

        function validate_type(val) {
            if (block_ptable.Type === "dos") {
                const hex_rx = /^[a-fA-F0-9]{2}$/;
                if (!hex_rx.test(val))
                    return _("Type must contain exactly two hexadecimal characters (0 to 9, A to F).");
            } else {
                /* We let people use any 128 bit value as a UUID, even
                   those that are not defined by RFC 4122. This is
                   what the rest of the storage stack does as well,
                   and the "BIOS boot" UUID is in fact invalid
                   according to RFC 4122.

                   But we do insist that the dashes are in the right
                   place, because UDisks2 does as well.
                */
                const uuid_rx_1 = /^[a-fA-F0-9-]*$/;
                if (!uuid_rx_1.test(val))
                    return _("Type can only contain the characters 0 to 9, A to F, and \"-\".");
                const uuid_rx_2 = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;
                if (!uuid_rx_2.test(val))
                    return _("Type must be of the form NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN.");
            }
        }

        dialog_open({
            Title: cockpit.format(_("Set partition type of $0"), block_name(block)),
            Fields: [
                SelectOne("type", _("Type"),
                          {
                              value: (type_names[type] ? type : "custom"),
                              choices,
                          }),
                TextInput("custom", _("Custom type"),
                          {
                              value: type,
                              validate: validate_type,
                              visible: vals => vals.type === "custom",
                          }),
            ],
            Action: {
                Danger: !client.in_anaconda_mode() && _("Changing partition types might prevent the system from booting."),
                Title: _("Save"),
                action: async function (vals) {
                    let t = vals.type === "custom" ? vals.custom : vals.type;
                    if (block_ptable?.Type === "dos")
                        t = "0x" + t;
                    await block_part.SetType(t, { });
                }
            },
        });
    }

    return (
        <StorageCard card={card}
                     alert={unused_space &&
                     <Alert variant="warning" isInline
                                   title={_("This partition is not completely used by its content.")}>
                         {cockpit.format(_("Partition size is $0. Content size is $1."),
                                         fmt_size(unused_space_warning.volume_size),
                                         fmt_size(unused_space_warning.content_size))}
                         <div className='storage-alert-actions'>
                             <StorageButton onClick={shrink_to_fit}>
                                 {_("Shrink partition")}
                             </StorageButton>
                             <StorageButton onClick={grow_to_fit}>
                                 {_("Grow content")}
                             </StorageButton>
                         </div>
                     </Alert>}>
            <CardBody>
                <DescriptionList className="pf-m-horizontal-on-sm">
                    <StorageDescription title={_("Name")} value={block_part.Name || "-"} />
                    <StorageDescription title={_("UUID")} value={block_part.UUID} />
                    <StorageDescription title={_("Type")}
                                        value={type_names[type] || type}
                                        action={<StorageLink onClick={set_type_dialog}>
                                            {_("edit")}
                                        </StorageLink>} />
                    { !unused_space &&
                    <StorageDescription title={_("Size")} value={fmt_size(block_part.Size)} />
                    }
                </DescriptionList>
            </CardBody>
        </StorageCard>);
};