feat(sd): add filesystem type selection for SD card format (exFAT/FAT32)

Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
luckfox-eng29
2026-04-29 17:41:56 +08:00
parent a3f65e4893
commit b7cf769cb2
4 changed files with 135 additions and 57 deletions

View File

@@ -67,6 +67,8 @@ interface StorageFilePageProps {
onUnmountSDStorage?: () => void; onUnmountSDStorage?: () => void;
onFormatSDStorage?: () => void; onFormatSDStorage?: () => void;
onMountSDStorage?: () => void; onMountSDStorage?: () => void;
fsType?: 'exfat' | 'fat32';
onFsTypeChange?: (value: 'exfat' | 'fat32') => void;
} }
export const FileManager: React.FC<StorageFilePageProps> = ({ export const FileManager: React.FC<StorageFilePageProps> = ({
@@ -79,6 +81,8 @@ export const FileManager: React.FC<StorageFilePageProps> = ({
onResetSDStorage, onResetSDStorage,
onUnmountSDStorage, onUnmountSDStorage,
onFormatSDStorage, onFormatSDStorage,
fsType,
onFsTypeChange,
}) => { }) => {
const { $at } = useReactAt(); const { $at } = useReactAt();
@@ -251,15 +255,28 @@ export const FileManager: React.FC<StorageFilePageProps> = ({
</p> </p>
{sdMountStatus !== "none" && ( {sdMountStatus !== "none" && (
<div className="pt-2"> <div className="pt-2">
<AntdButton <div className="w-full space-y-2">
disabled={loading} <p className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
danger={true} {$at("Choose the file system for MicroSD formatting")}
type="primary" </p>
onClick={handleFormatWrapper} <select
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800" value={fsType || "fat32"}
> onChange={(e) => onFsTypeChange?.(e.target.value as 'exfat' | 'fat32')}
{$at("Format MicroSD Card")} style={{ width: "100%", padding: "8px", borderRadius: "4px" }}
</AntdButton> >
<option value="fat32">FAT32</option>
<option value="exfat">exFAT</option>
</select>
<AntdButton
disabled={loading}
danger={true}
type="primary"
onClick={handleFormatWrapper}
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800"
>
{$at("Format MicroSD Card")} ({(fsType || "fat32")})
</AntdButton>
</div>
</div> </div>
)} )}
</div> </div>
@@ -318,6 +335,8 @@ export const FileManager: React.FC<StorageFilePageProps> = ({
onUnmountSDStorage={handleUnmountWrapper} onUnmountSDStorage={handleUnmountWrapper}
onFormatSDStorage={handleFormatWrapper} onFormatSDStorage={handleFormatWrapper}
syncStorage={syncStorage} syncStorage={syncStorage}
fsType={fsType}
onFsTypeChange={onFsTypeChange}
/> />
{uploadFile ? ( {uploadFile ? (
@@ -504,29 +523,46 @@ interface ActionButtonsSectionProps {
onUnmountSDStorage?: () => void; onUnmountSDStorage?: () => void;
onFormatSDStorage?: () => void; onFormatSDStorage?: () => void;
syncStorage: () => void; syncStorage: () => void;
fsType?: 'exfat' | 'fat32';
onFsTypeChange?: (value: 'exfat' | 'fat32') => void;
} }
const ActionButtonsSection: React.FC<ActionButtonsSectionProps> = ({ const ActionButtonsSection: React.FC<ActionButtonsSectionProps> = ({
mediaType, mediaType,
loading, loading,
showSDManagement, showSDManagement,
onUnmountSDStorage, onUnmountSDStorage,
onFormatSDStorage, onFormatSDStorage,
}) => { fsType,
onFsTypeChange,
}) => {
const { $at } = useReactAt(); const { $at } = useReactAt();
if (mediaType === "sd" && showSDManagement) { if (mediaType === "sd" && showSDManagement) {
return ( return (
<div className="flex animate-fadeIn justify-between gap-2 opacity-0" <div className="animate-fadeIn space-y-2 opacity-0"
style={{ animationDuration: "0.7s", animationDelay: "0.25s" }} style={{ animationDuration: "0.7s", animationDelay: "0.25s" }}
> >
<AntdButton <div className="w-full space-y-2">
disabled={loading} <p className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
type="primary" {$at("Choose the file system for MicroSD formatting")}
danger={true} </p>
onClick={onFormatSDStorage} <select
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800" value={fsType || "fat32"}
>{$at("Format MicroSD Card")}</AntdButton> onChange={(e) => onFsTypeChange?.(e.target.value as 'exfat' | 'fat32')}
style={{ width: "100%", padding: "8px", borderRadius: "4px" }}
>
<option value="fat32">FAT32</option>
<option value="exfat">exFAT</option>
</select>
<AntdButton
disabled={loading}
type="primary"
danger={true}
onClick={onFormatSDStorage}
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800"
>{$at("Format MicroSD Card")} ({(fsType || "fat32")})</AntdButton>
</div>
<AntdButton <AntdButton
disabled={loading} disabled={loading}
type="primary" type="primary"

View File

@@ -9,6 +9,7 @@ export default function SDFilePage() {
const { $at } = useReactAt(); const { $at } = useReactAt();
const [send] = useJsonRpc(); const [send] = useJsonRpc();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [fsType, setFsType] = useState<'exfat' | 'fat32'>('fat32');
const handleResetSDStorage = async () => { const handleResetSDStorage = async () => {
setLoading(true); setLoading(true);
@@ -37,11 +38,11 @@ export default function SDFilePage() {
}; };
const handleFormatSDStorage = async () => { const handleFormatSDStorage = async () => {
if (!window.confirm($at("Formatting the SD card will erase all data. Continue?"))) { if (!window.confirm($at(`Formatting the SD card as ${fsType.toUpperCase()} will erase all data. Continue?`))) {
return; return;
} }
setLoading(true); setLoading(true);
send("formatSDStorage", { confirm: true }, res => { send("formatSDStorage", { confirm: true, fsType }, res => {
if ("error" in res) { if ("error" in res) {
notifications.error(res.error.data || res.error.message); notifications.error(res.error.data || res.error.message);
setLoading(false); setLoading(false);
@@ -54,17 +55,21 @@ export default function SDFilePage() {
}; };
return ( return (
<FileManager <>
mediaType="sd" <FileManager
returnTo="/sd-files" mediaType="sd"
listFilesMethod="listSDStorageFiles" returnTo="/sd-files"
getSpaceMethod="getSDStorageSpace" listFilesMethod="listSDStorageFiles"
deleteFileMethod="deleteSDStorageFile" getSpaceMethod="getSDStorageSpace"
downloadUrlPrefix="/storage/sd-download" deleteFileMethod="deleteSDStorageFile"
showSDManagement={true} downloadUrlPrefix="/storage/sd-download"
onResetSDStorage={handleResetSDStorage} showSDManagement={true}
onUnmountSDStorage={handleUnmountSDStorage} onResetSDStorage={handleResetSDStorage}
onFormatSDStorage={handleFormatSDStorage} onUnmountSDStorage={handleUnmountSDStorage}
/> onFormatSDStorage={handleFormatSDStorage}
fsType={fsType}
onFsTypeChange={setFsType}
/>
</>
); );
} }

View File

@@ -105,6 +105,7 @@ export default function ImageManager({
const [sdMountStatus, setSDMountStatus] = useState<"ok" | "none" | "fail" | null>(storageType === 'sd' ? null : 'ok'); const [sdMountStatus, setSDMountStatus] = useState<"ok" | "none" | "fail" | null>(storageType === 'sd' ? null : 'ok');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [uploadFile, setUploadFile] = useState<string | null>(null); const [uploadFile, setUploadFile] = useState<string | null>(null);
const [fsType, setFsType] = useState<'exfat' | 'fat32'>('fat32');
const filesPerPage = 5; const filesPerPage = 5;
const percentageUsed = useMemo(() => { const percentageUsed = useMemo(() => {
@@ -170,7 +171,7 @@ export default function ImageManager({
return; return;
} }
setLoading(true); setLoading(true);
send("formatSDStorage", { confirm: true }, res => { send("formatSDStorage", { confirm: true, fsType }, res => {
if ("error" in res) { if ("error" in res) {
notifications.error(res.error.data || res.error.message); notifications.error(res.error.data || res.error.message);
setLoading(false); setLoading(false);
@@ -347,15 +348,28 @@ export default function ImageManager({
</p> </p>
{sdMountStatus !== "none" && ( {sdMountStatus !== "none" && (
<div className="pt-2"> <div className="pt-2">
<AntdButton <div className="mx-auto w-full max-w-[360px] space-y-2">
disabled={loading} <p className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
danger={true} {$at("Choose the file system for MicroSD formatting")}
type="primary" </p>
onClick={handleFormatSDStorage} <select
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800" value={fsType}
> onChange={(e) => setFsType(e.target.value as 'exfat' | 'fat32')}
{$at("Format MicroSD Card")} style={{ width: "100%", padding: "8px", borderRadius: "4px" }}
</AntdButton> >
<option value="fat32">FAT32</option>
<option value="exfat">exFAT</option>
</select>
<AntdButton
disabled={loading}
danger={true}
type="primary"
onClick={handleFormatSDStorage}
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800"
>
{$at("Format MicroSD Card")} ({fsType})
</AntdButton>
</div>
</div> </div>
)} )}
</div> </div>
@@ -493,16 +507,29 @@ export default function ImageManager({
</div> </div>
{unmountApi && storageType === 'sd' && ( {unmountApi && storageType === 'sd' && (
<div className="flex animate-fadeIn justify-between gap-2 opacity-0" <div className="animate-fadeIn space-y-2 opacity-0"
style={{ animationDuration: "0.7s", animationDelay: "0.25s" }} style={{ animationDuration: "0.7s", animationDelay: "0.25s" }}
> >
<AntdButton <div className="w-full space-y-2">
disabled={loading} <p className="w-full text-left text-xs text-slate-700 dark:text-slate-300">
type="primary" {$at("Choose the file system for MicroSD formatting")}
danger={true} </p>
onClick={handleFormatSDStorage} <select
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800" value={fsType}
>{$at("Format MicroSD Card")}</AntdButton> onChange={(e) => setFsType(e.target.value as 'exfat' | 'fat32')}
style={{ width: "100%", padding: "8px", borderRadius: "4px" }}
>
<option value="fat32">FAT32</option>
<option value="exfat">exFAT</option>
</select>
<AntdButton
disabled={loading}
type="primary"
danger={true}
onClick={handleFormatSDStorage}
className="w-full text-red-500 dark:text-red-400 border-red-200 dark:border-red-800"
>{$at("Format MicroSD Card")} ({fsType})</AntdButton>
</div>
<AntdButton <AntdButton
disabled={loading} disabled={loading}
type="primary" type="primary"

View File

@@ -782,7 +782,11 @@ func rpcUnmountSDStorage() error {
return nil return nil
} }
func rpcFormatSDStorage(confirm bool) error { func rpcFormatSDStorage(confirm bool, fsType string) error {
validFsTypes := map[string]bool{"exfat": true, "fat32": true}
if !validFsTypes[fsType] {
fsType = "fat32"
}
if !confirm { if !confirm {
return fmt.Errorf("format not confirmed") return fmt.Errorf("format not confirmed")
} }
@@ -864,7 +868,13 @@ func rpcFormatSDStorage(confirm bool) error {
return fmt.Errorf("failed to stat sd partition: %w", err) return fmt.Errorf("failed to stat sd partition: %w", err)
} }
mkfsOut, mkfsErr := exec.Command("mkfs.vfat", "-F", "32", "-n", "PICOKVM", "/dev/mmcblk1p1").CombinedOutput() var mkfsCmd *exec.Cmd
if fsType == "exfat" {
mkfsCmd = exec.Command("mkfs.exfat", "-n", "PICOKVM", "/dev/mmcblk1p1")
} else {
mkfsCmd = exec.Command("mkfs.vfat", "-F", "32", "-n", "PICOKVM", "/dev/mmcblk1p1")
}
mkfsOut, mkfsErr := mkfsCmd.CombinedOutput()
if mkfsErr != nil { if mkfsErr != nil {
return fmt.Errorf("failed to format sdcard: %w: %s", mkfsErr, strings.TrimSpace(string(mkfsOut))) return fmt.Errorf("failed to format sdcard: %w: %s", mkfsErr, strings.TrimSpace(string(mkfsOut)))
} }