mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-16 03:04:27 -04:00
[ENG-765] Reported Total capacity and Free space are wrong (#1066)
* Attempt at fixing stats * Fix macOS disk stats retrieve logic - Ignore mounted dmgs when calculation disk total/free size - Only take into account disk mounted by macOS * macos only import * Fix Linux * Replace byte-size with a custom implementation that supports BigInt * Fix NaN in Statistics * clippy * fmt * Move linux get_volumes to a specilized function - Fix ZFS handling - Improve handling of disk symlinks and multiple mounts * Fix macOS
This commit is contained in:
committed by
GitHub
parent
7067dcb35c
commit
dac54e44d1
2
.github/scripts/osxcross/README.md
vendored
2
.github/scripts/osxcross/README.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
This container based on alpine 3.17, with the most common build decencies installed, and a built version of [`osxcross`](https://github.com/tpoechtrager/osxcross) plus the macOS SDK 12.3 (Monterey) targeting a minimum compatibility of macOS 10.15 (Catalina) for x86_64 and macOS 11.0 (BigSur) for arm64.
|
||||
|
||||
__Image Tag__: macOS SDK version + osxcross commit hash + revision
|
||||
**Image Tag**: macOS SDK version + osxcross commit hash + revision
|
||||
|
||||
This container is currently available at:
|
||||
https://hub.docker.com/r/vvasconcellos/osxcross.
|
||||
|
||||
BIN
Cargo.lock
generated
BIN
Cargo.lock
generated
Binary file not shown.
@@ -30,7 +30,6 @@
|
||||
"@sd/client": "workspace:*",
|
||||
"@shopify/flash-list": "1.4.2",
|
||||
"@tanstack/react-query": "^4.29.1",
|
||||
"byte-size": "^8.1.0",
|
||||
"class-variance-authority": "^0.5.3",
|
||||
"dayjs": "^1.11.8",
|
||||
"expo": "~48.0.19",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import byteSize from 'byte-size';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Copy,
|
||||
@@ -13,7 +12,7 @@ import {
|
||||
} from 'phosphor-react-native';
|
||||
import { PropsWithChildren, useRef } from 'react';
|
||||
import { Pressable, Text, View, ViewStyle } from 'react-native';
|
||||
import { bytesToNumber, getItemFilePath, getItemObject } from '@sd/client';
|
||||
import { byteSize, getItemFilePath, getItemObject } from '@sd/client';
|
||||
import FileThumb from '~/components/explorer/FileThumb';
|
||||
import FavoriteButton from '~/components/explorer/sections/FavoriteButton';
|
||||
import InfoTagPills from '~/components/explorer/sections/InfoTagPills';
|
||||
@@ -85,12 +84,7 @@ export const ActionsModal = () => {
|
||||
</Text>
|
||||
<View style={tw`flex flex-row`}>
|
||||
<Text style={tw`text-xs text-ink-faint`}>
|
||||
{filePath?.size_in_bytes_bytes
|
||||
? byteSize(
|
||||
bytesToNumber(filePath.size_in_bytes_bytes)
|
||||
).toString()
|
||||
: 0}
|
||||
,
|
||||
{`${byteSize(filePath?.size_in_bytes_bytes)}`},
|
||||
</Text>
|
||||
<Text style={tw`text-xs text-ink-faint`}>
|
||||
{' '}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import byteSize from 'byte-size';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Barcode,
|
||||
@@ -13,7 +12,7 @@ import { forwardRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import {
|
||||
ExplorerItem,
|
||||
bytesToNumber,
|
||||
byteSize,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
useLibraryQuery
|
||||
@@ -97,13 +96,7 @@ const FileInfoModal = forwardRef<ModalRef, FileInfoModalProps>((props, ref) => {
|
||||
<MetaItem
|
||||
title="Size"
|
||||
icon={Cube}
|
||||
value={
|
||||
filePathData?.size_in_bytes_bytes
|
||||
? byteSize(
|
||||
bytesToNumber(filePathData.size_in_bytes_bytes)
|
||||
).toString()
|
||||
: 0
|
||||
}
|
||||
value={`${byteSize(filePathData?.size_in_bytes_bytes)}`}
|
||||
/>
|
||||
{/* Duration */}
|
||||
{fullObjectData.data?.media_data?.duration_seconds && (
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import byteSize from 'byte-size';
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { ScrollView, Text, View } from 'react-native';
|
||||
import RNFS from 'react-native-fs';
|
||||
import { Statistics, useLibraryQuery } from '@sd/client';
|
||||
import { Statistics, byteSize, useLibraryQuery } from '@sd/client';
|
||||
import useCounter from '~/hooks/useCounter';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
@@ -28,9 +27,9 @@ const EMPTY_STATISTICS = {
|
||||
};
|
||||
|
||||
const StatItem: FC<{ title: string; bytes: bigint }> = ({ title, bytes }) => {
|
||||
const { value, unit } = byteSize(Number(bytes)); // TODO: This BigInt to Number conversion will truncate the number if the number is too large. `byteSize` doesn't support BigInt so we are gonna need to come up with a longer term solution at some point.
|
||||
const { value, unit } = byteSize(bytes);
|
||||
|
||||
const count = useCounter({ name: title, end: Number(value) });
|
||||
const count = useCounter({ name: title, end: value });
|
||||
|
||||
return (
|
||||
<View style={tw`flex flex-col p-4`}>
|
||||
|
||||
@@ -10,8 +10,10 @@ edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mobile = [] # This feature allows features to be disabled when the Core is running on mobile.
|
||||
ffmpeg = ["dep:sd-ffmpeg"] # This feature controls whether the Spacedrive Core contains functionality which requires FFmpeg.
|
||||
# This feature allows features to be disabled when the Core is running on mobile.
|
||||
mobile = []
|
||||
# This feature controls whether the Spacedrive Core contains functionality which requires FFmpeg.
|
||||
ffmpeg = ["dep:sd-ffmpeg"]
|
||||
location-watcher = ["dep:notify"]
|
||||
sync-messages = []
|
||||
heif = ["dep:sd-heif"]
|
||||
@@ -46,6 +48,7 @@ tokio = { workspace = true, features = [
|
||||
"io-util",
|
||||
"macros",
|
||||
"time",
|
||||
"process",
|
||||
] }
|
||||
|
||||
base64 = "0.21.2"
|
||||
@@ -93,6 +96,9 @@ hex = "0.4.3"
|
||||
int-enum = "0.5.0"
|
||||
tokio-stream = "0.1.14"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
plist = "1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.winapi-util]
|
||||
version = "0.1.5"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{
|
||||
library::{LibraryConfig, LibraryName},
|
||||
prisma::statistics,
|
||||
util::MaybeUndefined,
|
||||
volume::{get_volumes, save_volume},
|
||||
volume::get_volumes,
|
||||
};
|
||||
|
||||
use chrono::Utc;
|
||||
@@ -26,25 +26,22 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
})
|
||||
.procedure("statistics", {
|
||||
R.with2(library()).query(|(_, library), _: ()| async move {
|
||||
let _statistics = library
|
||||
.db
|
||||
.statistics()
|
||||
.find_unique(statistics::id::equals(library.node_local_id))
|
||||
.exec()
|
||||
.await?;
|
||||
// TODO: get from database if library is offline
|
||||
// let _statistics = library
|
||||
// .db
|
||||
// .statistics()
|
||||
// .find_unique(statistics::id::equals(library.node_local_id))
|
||||
// .exec()
|
||||
// .await?;
|
||||
|
||||
// TODO: get from database, not sys
|
||||
let volumes = get_volumes();
|
||||
save_volume(&library).await?;
|
||||
let volumes = get_volumes().await;
|
||||
// save_volume(&library).await?;
|
||||
|
||||
let mut available_capacity: u64 = 0;
|
||||
let mut total_capacity: u64 = 0;
|
||||
|
||||
if let Ok(volumes) = volumes {
|
||||
for volume in volumes {
|
||||
total_capacity += volume.total_capacity;
|
||||
available_capacity += volume.available_capacity;
|
||||
}
|
||||
let mut available_capacity: u64 = 0;
|
||||
for volume in volumes {
|
||||
total_capacity += volume.total_capacity;
|
||||
available_capacity += volume.available_capacity;
|
||||
}
|
||||
|
||||
let library_db_size = get_size(
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
library::{Category, Library},
|
||||
location::{
|
||||
file_path_helper::{check_file_path_exists, IsolatedFilePathData},
|
||||
find_location, LocationError,
|
||||
LocationError,
|
||||
},
|
||||
object::preview::get_thumb_key,
|
||||
prisma::{self, file_path, location, object, tag, tag_on_object, PrismaClient},
|
||||
|
||||
@@ -6,6 +6,6 @@ use super::{Ctx, R};
|
||||
|
||||
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||
R.router().procedure("list", {
|
||||
R.query(|_, _: ()| async move { Ok(get_volumes()?) })
|
||||
R.query(|_, _: ()| async move { Ok(get_volumes().await) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use crate::{
|
||||
library::Library,
|
||||
prisma::volume::{self, *},
|
||||
};
|
||||
// Adapted from: https://github.com/kimlimjustin/xplorer/blob/f4f3590d06783d64949766cc2975205a3b689a56/src-tauri/src/drives.rs
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use specta::Type;
|
||||
use std::{fmt::Display, process::Command};
|
||||
use std::{ffi::OsString, fmt::Display, path::PathBuf, sync::OnceLock};
|
||||
use sysinfo::{DiskExt, System, SystemExt};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::error;
|
||||
|
||||
fn sys_guard() -> &'static Mutex<System> {
|
||||
static SYS: OnceLock<Mutex<System>> = OnceLock::new();
|
||||
SYS.get_or_init(|| Mutex::new(System::new_all()))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
@@ -31,16 +35,15 @@ impl Display for DiskType {
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
|
||||
pub struct Volume {
|
||||
pub name: String,
|
||||
pub mount_point: String,
|
||||
pub name: OsString,
|
||||
pub mount_points: Vec<PathBuf>,
|
||||
#[specta(type = String)]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub total_capacity: u64,
|
||||
#[specta(type = String)]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub available_capacity: u64,
|
||||
pub is_removable: bool,
|
||||
pub disk_type: Option<DiskType>,
|
||||
pub disk_type: DiskType,
|
||||
pub file_system: Option<String>,
|
||||
pub is_root_filesystem: bool,
|
||||
}
|
||||
@@ -59,102 +62,307 @@ impl From<VolumeError> for rspc::Error {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_volume(library: &Library) -> Result<(), VolumeError> {
|
||||
let volumes = get_volumes()?;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub async fn get_volumes() -> Vec<Volume> {
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
// enter all volumes associate with this client add to db
|
||||
for volume in volumes {
|
||||
let params = vec![
|
||||
disk_type::set(volume.disk_type.map(|t| t.to_string())),
|
||||
filesystem::set(volume.file_system.clone()),
|
||||
total_bytes_capacity::set(volume.total_capacity.to_string()),
|
||||
total_bytes_available::set(volume.available_capacity.to_string()),
|
||||
];
|
||||
let mut sys = sys_guard().lock().await;
|
||||
sys.refresh_disks_list();
|
||||
|
||||
library
|
||||
.db
|
||||
.volume()
|
||||
.upsert(
|
||||
node_id_mount_point_name(
|
||||
library.node_local_id,
|
||||
volume.mount_point.to_string(),
|
||||
volume.name.to_string(),
|
||||
),
|
||||
volume::create(
|
||||
library.node_local_id,
|
||||
volume.name,
|
||||
volume.mount_point,
|
||||
params.clone(),
|
||||
),
|
||||
params,
|
||||
)
|
||||
.exec()
|
||||
.await?;
|
||||
}
|
||||
// cleanup: remove all unmodified volumes associate with this client
|
||||
let mut volumes: Vec<Volume> = Vec::new();
|
||||
let mut path_to_volume_index = HashMap::new();
|
||||
for disk in sys.disks() {
|
||||
let disk_name = disk.name();
|
||||
let mount_point = disk.mount_point().to_path_buf();
|
||||
let file_system = String::from_utf8(disk.file_system().to_vec())
|
||||
.map(|s| s.to_uppercase())
|
||||
.ok();
|
||||
let total_capacity = disk.total_space();
|
||||
let available_capacity = disk.available_space();
|
||||
let is_root_filesystem = mount_point.is_absolute() && mount_point.parent().is_none();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let mut disk_path: PathBuf = PathBuf::from(disk_name);
|
||||
if file_system.as_ref().map(|fs| fs == "ZFS").unwrap_or(false) {
|
||||
// Use a custom path for ZFS disks to avoid conflicts with normal disks paths
|
||||
disk_path = Path::new("zfs://").join(disk_path);
|
||||
} else {
|
||||
// Ignore non-devices disks (overlay, fuse, tmpfs, etc.)
|
||||
if !disk_path.starts_with("/dev") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Error handling in this function
|
||||
pub fn get_volumes() -> Result<Vec<Volume>, VolumeError> {
|
||||
System::new_all()
|
||||
.disks()
|
||||
.iter()
|
||||
.filter_map(|disk| {
|
||||
let mut total_capacity = disk.total_space();
|
||||
let mount_point = disk.mount_point().to_str().unwrap_or("/").to_string();
|
||||
let available_capacity = disk.available_space();
|
||||
let name = disk.name().to_str().unwrap_or("Volume").to_string();
|
||||
let is_removable = disk.is_removable();
|
||||
|
||||
let file_system = String::from_utf8(disk.file_system().to_vec())
|
||||
.unwrap_or_else(|_| "Err".to_string());
|
||||
|
||||
let disk_type = match disk.type_() {
|
||||
sysinfo::DiskType::SSD => DiskType::SSD,
|
||||
sysinfo::DiskType::HDD => DiskType::HDD,
|
||||
_ => DiskType::Removable,
|
||||
// Ensure disk has a valid device path
|
||||
let real_path = match tokio::fs::canonicalize(disk_name).await {
|
||||
Err(real_path) => {
|
||||
error!(
|
||||
"Failed to canonicalize disk path {}: {:#?}",
|
||||
disk_name.to_string_lossy(),
|
||||
real_path
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(real_path) => real_path,
|
||||
};
|
||||
|
||||
if total_capacity < available_capacity && cfg!(target_os = "windows") {
|
||||
let mut caption = mount_point.clone();
|
||||
// Check if disk is a symlink to another disk
|
||||
if real_path != disk_path {
|
||||
// Disk is a symlink to another disk, assign it to the same volume
|
||||
path_to_volume_index.insert(
|
||||
real_path.into_os_string(),
|
||||
path_to_volume_index
|
||||
.get(disk_name)
|
||||
.cloned()
|
||||
.unwrap_or(path_to_volume_index.len()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(volume_index) = path_to_volume_index.get(disk_name) {
|
||||
// Disk already has a volume assigned, update it
|
||||
let volume: &mut Volume = volumes
|
||||
.get_mut(*volume_index)
|
||||
.expect("Volume index is present so the Volume must be present too");
|
||||
|
||||
// Update mount point if not already present
|
||||
if volume.mount_points.iter().all(|p| p != &mount_point) {
|
||||
volume.mount_points.push(mount_point);
|
||||
if !volume.is_root_filesystem {
|
||||
volume.is_root_filesystem = is_root_filesystem;
|
||||
}
|
||||
}
|
||||
|
||||
// Update mount capacity, it can change between mounts due to quotas (ZFS, BTRFS?)
|
||||
if volume.total_capacity < total_capacity {
|
||||
volume.total_capacity = total_capacity;
|
||||
}
|
||||
|
||||
// This shouldn't change between mounts, but just in case
|
||||
if volume.available_capacity > available_capacity {
|
||||
volume.available_capacity = available_capacity;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Assign volume to disk path
|
||||
path_to_volume_index.insert(disk_path.into_os_string(), volumes.len());
|
||||
|
||||
volumes.push(Volume {
|
||||
name: disk_name.to_os_string(),
|
||||
disk_type: if disk.is_removable() {
|
||||
DiskType::Removable
|
||||
} else {
|
||||
match disk.type_() {
|
||||
sysinfo::DiskType::SSD => DiskType::SSD,
|
||||
sysinfo::DiskType::HDD => DiskType::HDD,
|
||||
_ => DiskType::Removable,
|
||||
}
|
||||
},
|
||||
file_system,
|
||||
mount_points: vec![mount_point],
|
||||
total_capacity,
|
||||
available_capacity,
|
||||
is_root_filesystem,
|
||||
});
|
||||
}
|
||||
|
||||
volumes
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct ImageSystemEntity {
|
||||
mount_point: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct ImageInfo {
|
||||
system_entities: Vec<ImageSystemEntity>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct HDIUtilInfo {
|
||||
images: Vec<ImageInfo>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub async fn get_volumes() -> Vec<Volume> {
|
||||
use futures::future;
|
||||
use tokio::process::Command;
|
||||
|
||||
let mut sys = sys_guard().lock().await;
|
||||
sys.refresh_disks_list();
|
||||
|
||||
// Ignore mounted DMGs
|
||||
#[cfg(target_os = "macos")]
|
||||
let dmgs = &Command::new("hdiutil")
|
||||
.args(["info", "-plist"])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|err| error!("Failed to execute hdiutil: {err:#?}"))
|
||||
.ok()
|
||||
.and_then(|wmic_process| {
|
||||
use std::str::FromStr;
|
||||
|
||||
if wmic_process.status.success() {
|
||||
let info: Result<HDIUtilInfo, _> = plist::from_bytes(&wmic_process.stdout);
|
||||
match info {
|
||||
Err(err) => {
|
||||
error!("Failed to parse hdiutil output: {err:#?}");
|
||||
None
|
||||
}
|
||||
Ok(info) => Some(
|
||||
info.images
|
||||
.into_iter()
|
||||
.flat_map(|image| image.system_entities)
|
||||
.flat_map(|entity: ImageSystemEntity| entity.mount_point)
|
||||
.flat_map(|mount_point| PathBuf::from_str(mount_point.as_str()))
|
||||
.collect::<std::collections::HashSet<_>>(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
error!("Command hdiutil return error");
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
future::join_all(sys.disks().iter().map(|disk| async {
|
||||
let disk_name = disk.name();
|
||||
let mount_point = disk.mount_point().to_path_buf();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Ignore mounted DMGs
|
||||
if dmgs
|
||||
.as_ref()
|
||||
.map(|dmgs| dmgs.contains(&mount_point))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if !(mount_point.starts_with("/Volumes") || mount_point.starts_with("/System/Volumes"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_mut)] // mut is used in windows
|
||||
let mut total_capacity = disk.total_space();
|
||||
let available_capacity = disk.available_space();
|
||||
let is_root_filesystem = mount_point.is_absolute() && mount_point.parent().is_none();
|
||||
|
||||
// Fix broken google drive partition size in Windows
|
||||
#[cfg(windows)]
|
||||
if total_capacity < available_capacity && is_root_filesystem {
|
||||
// Use available capacity as total capacity in the case we can't get the correct value
|
||||
total_capacity = available_capacity;
|
||||
|
||||
let caption = mount_point.to_str();
|
||||
if let Some(caption) = caption {
|
||||
let mut caption = caption.to_string();
|
||||
|
||||
// Remove path separator from Disk letter
|
||||
caption.pop();
|
||||
let wmic_process = Command::new("cmd")
|
||||
|
||||
let wmic_output = Command::new("cmd")
|
||||
.args([
|
||||
"/C",
|
||||
&format!("wmic logical disk where Caption='{caption}' get Size"),
|
||||
])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
let wmic_process_output = String::from_utf8(wmic_process.stdout).ok()?;
|
||||
let parsed_size =
|
||||
wmic_process_output.split("\r\r\n").collect::<Vec<&str>>()[1].to_string();
|
||||
.await
|
||||
.map_err(|err| error!("Failed to execute hdiutil: {err:#?}"))
|
||||
.ok()
|
||||
.and_then(|wmic_process| {
|
||||
if wmic_process.status.success() {
|
||||
String::from_utf8(wmic_process.stdout).ok()
|
||||
} else {
|
||||
error!("Command wmic return error");
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(n) = parsed_size.trim().parse::<u64>() {
|
||||
total_capacity = n;
|
||||
if let Some(wmic_output) = wmic_output {
|
||||
match wmic_output.split("\r\r\n").collect::<Vec<&str>>()[1]
|
||||
.to_string()
|
||||
.trim()
|
||||
.parse::<u64>()
|
||||
{
|
||||
Err(err) => error!("Failed to parse wmic output: {err:#?}"),
|
||||
Ok(n) => total_capacity = n,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(!mount_point.starts_with("/System")).then_some(Ok(Volume {
|
||||
name,
|
||||
is_root_filesystem: mount_point == "/",
|
||||
mount_point,
|
||||
total_capacity,
|
||||
available_capacity,
|
||||
is_removable,
|
||||
disk_type: Some(disk_type),
|
||||
file_system: Some(file_system),
|
||||
}))
|
||||
Some(Volume {
|
||||
name: disk_name.to_os_string(),
|
||||
disk_type: if disk.is_removable() {
|
||||
DiskType::Removable
|
||||
} else {
|
||||
match disk.type_() {
|
||||
sysinfo::DiskType::SSD => DiskType::SSD,
|
||||
sysinfo::DiskType::HDD => DiskType::HDD,
|
||||
_ => DiskType::Removable,
|
||||
}
|
||||
},
|
||||
mount_points: vec![mount_point],
|
||||
file_system: String::from_utf8(disk.file_system().to_vec()).ok(),
|
||||
total_capacity,
|
||||
available_capacity,
|
||||
is_root_filesystem,
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<Volume>>()
|
||||
}
|
||||
|
||||
// pub async fn save_volume(library: &Library) -> Result<(), VolumeError> {
|
||||
// // enter all volumes associate with this client add to db
|
||||
// for volume in get_volumes() {
|
||||
// let params = vec![
|
||||
// disk_type::set(volume.disk_type.map(|t| t.to_string())),
|
||||
// filesystem::set(volume.file_system.clone()),
|
||||
// total_bytes_capacity::set(volume.total_capacity.to_string()),
|
||||
// total_bytes_available::set(volume.available_capacity.to_string()),
|
||||
// ];
|
||||
|
||||
// library
|
||||
// .db
|
||||
// .volume()
|
||||
// .upsert(
|
||||
// node_id_mount_point_name(
|
||||
// library.node_local_id,
|
||||
// volume.mount_point,
|
||||
// volume.name,
|
||||
// ),
|
||||
// volume::create(
|
||||
// library.node_local_id,
|
||||
// volume.name,
|
||||
// volume.mount_point,
|
||||
// params.clone(),
|
||||
// ),
|
||||
// params,
|
||||
// )
|
||||
// .exec()
|
||||
// .await?;
|
||||
// }
|
||||
// // cleanup: remove all unmodified volumes associate with this client
|
||||
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn test_get_volumes() {
|
||||
// let volumes = get_volumes()?;
|
||||
// dbg!(&volumes);
|
||||
// assert!(volumes.len() > 0);
|
||||
// }
|
||||
|
||||
// Adapted from: https://github.com/kimlimjustin/xplorer/blob/f4f3590d06783d64949766cc2975205a3b689a56/src-tauri/src/drives.rs
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// import types from '../../constants/file-types.json';
|
||||
import { Image, Image_Light } from '@sd/assets/icons';
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { Barcode, CircleWavyCheck, Clock, Cube, Hash, Link, Lock, Snowflake } from 'phosphor-react';
|
||||
@@ -10,7 +9,7 @@ import {
|
||||
Location,
|
||||
ObjectKind,
|
||||
Tag,
|
||||
bytesToNumber,
|
||||
byteSize,
|
||||
getItemFilePath,
|
||||
getItemObject,
|
||||
isPath,
|
||||
@@ -157,9 +156,7 @@ export const Inspector = ({ data, context, showThumbnail = true, ...props }: Pro
|
||||
<InspectorIcon component={Cube} />
|
||||
<span className="mr-1.5">Size</span>
|
||||
<MetaValue>
|
||||
{byteSize(
|
||||
bytesToNumber(filePathData.size_in_bytes_bytes)
|
||||
).toString()}
|
||||
{`${byteSize(filePathData.size_in_bytes_bytes)}`}
|
||||
</MetaValue>
|
||||
</MetaTextLine>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import { memo } from 'react';
|
||||
import { ExplorerItem, bytesToNumber, getItemFilePath, getItemLocation } from '@sd/client';
|
||||
import GridList from '~/components/GridList';
|
||||
import { ExplorerItem, byteSize, getItemFilePath, getItemLocation } from '@sd/client';
|
||||
import { GridList } from '~/components';
|
||||
import { ViewItem } from '.';
|
||||
import FileThumb from '../FilePath/Thumb';
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
@@ -50,7 +49,7 @@ const GridViewItem = memo(({ data, selected, index, cut, ...props }: GridViewIte
|
||||
'cursor-default truncate rounded-md px-1.5 py-[1px] text-center text-tiny text-ink-dull '
|
||||
)}
|
||||
>
|
||||
{byteSize(bytesToNumber(filePathData.size_in_bytes_bytes)).toString()}
|
||||
{`${byteSize(filePathData.size_in_bytes_bytes)}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useReactTable
|
||||
} from '@tanstack/react-table';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { CaretDown, CaretUp } from 'phosphor-react';
|
||||
@@ -19,7 +18,7 @@ import {
|
||||
ExplorerItem,
|
||||
FilePath,
|
||||
ObjectKind,
|
||||
bytesToNumber,
|
||||
byteSize,
|
||||
getExplorerItemData,
|
||||
getItemFilePath,
|
||||
getItemLocation,
|
||||
@@ -210,7 +209,7 @@ export default () => {
|
||||
const file_path = getItemFilePath(file);
|
||||
if (!file_path || !file_path.size_in_bytes_bytes) return;
|
||||
|
||||
return byteSize(bytesToNumber(file_path.size_in_bytes_bytes));
|
||||
return byteSize(file_path.size_in_bytes_bytes);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ArrowsOutSimple } from 'phosphor-react';
|
||||
import { memo } from 'react';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { Button } from '@sd/ui';
|
||||
import GridList from '~/components/GridList';
|
||||
import { GridList } from '~/components';
|
||||
import { ViewItem } from '.';
|
||||
import FileThumb from '../FilePath/Thumb';
|
||||
import { useExplorerViewContext } from '../ViewContext';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import byteSize from 'byte-size';
|
||||
import clsx from 'clsx';
|
||||
import { Info } from 'phosphor-react';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import 'react-loading-skeleton/dist/skeleton.css';
|
||||
import { Statistics, useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||
import { Statistics, byteSize, useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||
import { Tooltip } from '@sd/ui';
|
||||
import { useCounter } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
@@ -46,12 +45,12 @@ const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof type
|
||||
let mounted = false;
|
||||
|
||||
const StatItem = (props: StatItemProps) => {
|
||||
const { title, bytes = BigInt('0'), isLoading } = props;
|
||||
const { title, bytes, isLoading } = props;
|
||||
|
||||
const size = byteSize(Number(bytes)); // TODO: This BigInt to Number conversion will truncate the number if the number is too large. `byteSize` doesn't support BigInt so we are gonna need to come up with a longer term solution at some point.
|
||||
const size = byteSize(bytes);
|
||||
const count = useCounter({
|
||||
name: title,
|
||||
end: +size.value,
|
||||
end: size.value,
|
||||
duration: mounted ? 0 : 1,
|
||||
saveState: false
|
||||
});
|
||||
|
||||
@@ -47,7 +47,10 @@ interface ResizeProps<T extends GridListSelection> extends GridListDefaults<T> {
|
||||
|
||||
type GridListProps<T extends GridListSelection> = WrapProps<T> | ResizeProps<T>;
|
||||
|
||||
export default <T extends GridListSelection>({ selectable = true, ...props }: GridListProps<T>) => {
|
||||
export const GridList = <T extends GridListSelection>({
|
||||
selectable = true,
|
||||
...props
|
||||
}: GridListProps<T>) => {
|
||||
const scrollBarWidth = 6;
|
||||
|
||||
const multiSelect = Array.isArray(props.selected);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useCountUp } from 'use-count-up';
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
|
||||
@@ -31,13 +31,24 @@ type UseCounterProps = {
|
||||
* default: `true`
|
||||
*/
|
||||
saveState?: boolean;
|
||||
/**
|
||||
* Number of decimal places. Defaults to `1`.
|
||||
*/
|
||||
precision?: number;
|
||||
/**
|
||||
* The locale to use for number formatting (e.g. `'de-DE'`).
|
||||
* Defaults to your system locale. Passed directed into [Intl.NumberFormat()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).
|
||||
*/
|
||||
locales?: string | string[];
|
||||
};
|
||||
|
||||
export const useCounter = ({
|
||||
name,
|
||||
start = 0,
|
||||
end,
|
||||
locales,
|
||||
duration = 2,
|
||||
precision = 1,
|
||||
saveState = true
|
||||
}: UseCounterProps) => {
|
||||
const { lastValue, setLastValue } = useCounterState(name);
|
||||
@@ -46,12 +57,23 @@ export const useCounter = ({
|
||||
start = lastValue;
|
||||
}
|
||||
|
||||
const formatter = useMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(locales, {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision
|
||||
}),
|
||||
[locales, precision]
|
||||
);
|
||||
|
||||
const { value } = useCountUp({
|
||||
isCounting: !(start === end),
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
easing: 'easeOutCubic'
|
||||
easing: 'easeOutCubic',
|
||||
formatter: (value) => formatter.format(value)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
"@types/react-scroll-sync": "^0.8.4",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"byte-size": "^8.1.0",
|
||||
"class-variance-authority": "^0.5.3",
|
||||
"clsx": "^1.2.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
@@ -72,7 +71,6 @@
|
||||
"devDependencies": {
|
||||
"@sd/config": "workspace:*",
|
||||
"@types/babel-core": "^6.25.7",
|
||||
"@types/byte-size": "^8.1.0",
|
||||
"@types/loadable__component": "^5.13.4",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/react": "^18.0.21",
|
||||
|
||||
@@ -264,4 +264,4 @@ export type TagCreateArgs = { name: string; color: string }
|
||||
|
||||
export type TagUpdateArgs = { id: number; name: string | null; color: string | null }
|
||||
|
||||
export type Volume = { name: string; mount_point: string; total_capacity: string; available_capacity: string; is_removable: boolean; disk_type: DiskType | null; file_system: string | null; is_root_filesystem: boolean }
|
||||
export type Volume = { name: string; mount_points: string[]; total_capacity: string; available_capacity: string; disk_type: DiskType; file_system: string | null; is_root_filesystem: boolean }
|
||||
|
||||
77
packages/client/src/lib/byte-size.ts
Normal file
77
packages/client/src/lib/byte-size.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// Inspired by: https://github.com/75lb/byte-size
|
||||
|
||||
const DECIMAL_UNITS = [
|
||||
{ short: 'B', long: 'bytes', from: 0n },
|
||||
{ short: 'kB', long: 'kilobytes', from: 1000n },
|
||||
{ short: 'MB', long: 'megabytes', from: 1000n ** 2n },
|
||||
{ short: 'GB', long: 'gigabytes', from: 1000n ** 3n },
|
||||
{ short: 'TB', long: 'terabytes', from: 1000n ** 4n },
|
||||
{ short: 'PB', long: 'petabytes', from: 1000n ** 5n },
|
||||
{ short: 'EB', long: 'exabytes', from: 1000n ** 6n },
|
||||
{ short: 'ZB', long: 'zettabytes', from: 1000n ** 7n },
|
||||
{ short: 'YB', long: 'yottabytes', from: 1000n ** 8n },
|
||||
{ short: 'RB', long: 'ronnabyte', from: 1000n ** 9n },
|
||||
{ short: 'QB', long: 'quettabyte', from: 1000n ** 10n }
|
||||
];
|
||||
|
||||
const getDecimalUnit = (n: bigint) => {
|
||||
const s = n.toString(10);
|
||||
const log10 = s.length + Math.log10(Number('0.' + s.substring(0, 15)));
|
||||
const index = (log10 / 3) | 0;
|
||||
return (
|
||||
DECIMAL_UNITS[index] ??
|
||||
(DECIMAL_UNITS[DECIMAL_UNITS.length - 1] as Exclude<
|
||||
(typeof DECIMAL_UNITS)[number],
|
||||
undefined
|
||||
>)
|
||||
);
|
||||
};
|
||||
|
||||
function bytesToNumber(bytes: string[] | number[] | bigint[]) {
|
||||
return bytes
|
||||
.map((b) => (typeof b === 'bigint' ? b : BigInt(b)))
|
||||
.reduce((acc, curr, i) => acc + curr * 256n ** BigInt(bytes.length - i - 1));
|
||||
}
|
||||
|
||||
export interface ByteSizeOpts {
|
||||
locales?: string | string[];
|
||||
precision: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with the spec `{ value: string, unit: string, long: string }`. The returned object defines a `toString` method meaning it can be used in any string context.
|
||||
*
|
||||
* @param value - The bytes value to convert.
|
||||
* @param options - Optional config.
|
||||
* @param options.locales - The locale to use for number formatting (e.g. `'de-DE'`). Defaults to your system locale. Passed directed into [Intl.NumberFormat()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).
|
||||
* @param options.precision - Number of decimal places. Defaults to `1`.
|
||||
*/
|
||||
export const byteSize = (
|
||||
value: null | string | number | bigint | string[] | number[] | bigint[] | undefined,
|
||||
{ precision, locales }: ByteSizeOpts = { precision: 1 }
|
||||
) => {
|
||||
if (value == null) value = 0n;
|
||||
if (Array.isArray(value)) value = bytesToNumber(value);
|
||||
else if (typeof value !== 'bigint') value = BigInt(value);
|
||||
const [isNegative, bytes] = value < 0n ? [true, -value] : [false, value];
|
||||
|
||||
const unit = getDecimalUnit(bytes);
|
||||
const defaultFormat = new Intl.NumberFormat(locales, {
|
||||
style: 'decimal',
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision
|
||||
});
|
||||
const precisionFactor = 10 ** precision;
|
||||
return {
|
||||
unit: unit.short,
|
||||
long: unit.long,
|
||||
value:
|
||||
(isNegative ? -1 : 1) *
|
||||
(unit.from === 0n
|
||||
? Number(bytes)
|
||||
: Number((bytes * BigInt(precisionFactor)) / unit.from) / precisionFactor),
|
||||
toString() {
|
||||
return `${defaultFormat.format(this.value)} ${this.unit}`;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './byte-size';
|
||||
export * from './passwordStrength';
|
||||
export * from './valito';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export function bytesToNumber(bytes: number[]) {
|
||||
return bytes.reduce((acc, curr, i) => acc + curr * Math.pow(256, bytes.length - i - 1), 0);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ExplorerItem } from '../core';
|
||||
|
||||
export * from './objectKind';
|
||||
export * from './formatBytes';
|
||||
export * from './explorerItem';
|
||||
// export * from './keys';
|
||||
|
||||
|
||||
BIN
pnpm-lock.yaml
generated
BIN
pnpm-lock.yaml
generated
Binary file not shown.
Reference in New Issue
Block a user