Fix for multi-platform compatibility (Windows FS sucks)

This commit is contained in:
grimsi
2025-04-05 21:46:26 +02:00
parent 0312b841d9
commit 3196a66d91
7 changed files with 89 additions and 18 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080"> <configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080">
<method v="2" /> <method v="2" />
</configuration> </configuration>
</component> </component>
@@ -5,6 +5,7 @@ import {FilesystemEndpoint} from "Frontend/generated/endpoints";
import FileDto from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/FileDto"; import FileDto from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/FileDto";
import FileType from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/FileType"; import FileType from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/FileType";
import {IFlatMetadata} from "react-accessible-treeview/dist/TreeView/utils"; import {IFlatMetadata} from "react-accessible-treeview/dist/TreeView/utils";
import OperatingSystemType from "Frontend/generated/de/grimsi/gameyfin/core/filesystem/OperatingSystemType";
interface ITreeNode<M extends IFlatMetadata = IFlatMetadata> { interface ITreeNode<M extends IFlatMetadata = IFlatMetadata> {
id?: NodeId; id?: NodeId;
@@ -14,7 +15,7 @@ interface ITreeNode<M extends IFlatMetadata = IFlatMetadata> {
metadata?: M; metadata?: M;
} }
export default function FileTreeView({setSelectedPath}: { setSelectedPath: (file: string) => void }) { export default function FileTreeView({onPathChange}: { onPathChange: (file: string) => void }) {
const rootNode: INode = { const rootNode: INode = {
id: "root", id: "root",
name: "", name: "",
@@ -22,14 +23,21 @@ export default function FileTreeView({setSelectedPath}: { setSelectedPath: (file
parent: null parent: null
} }
const [hostOSType, setHostOSType] = useState<OperatingSystemType>();
const [fileTree, setFileTree] = useState<ITreeNode>(); const [fileTree, setFileTree] = useState<ITreeNode>();
const [flattenedFileTree, setFlattenedFileTree] = useState<INode[]>([rootNode]); const [flattenedFileTree, setFlattenedFileTree] = useState<INode[]>([rootNode]);
useEffect(() => { useEffect(() => {
FilesystemEndpoint.listSubDirectories(undefined).then( FilesystemEndpoint.getHostOperatingSystem().then(
result => {
if (result === undefined) return;
setHostOSType(result);
}
)
FilesystemEndpoint.listSubDirectories("").then(
result => { result => {
if (result === undefined) return; if (result === undefined) return;
result = result.filter(r => r !== undefined);
const nodes = fileDtosToTree(result as FileDto[]); const nodes = fileDtosToTree(result as FileDto[]);
const tree = flattenTree(nodes); const tree = flattenTree(nodes);
setFileTree(nodes); setFileTree(nodes);
@@ -39,14 +47,25 @@ export default function FileTreeView({setSelectedPath}: { setSelectedPath: (file
}, []); }, []);
function getAbsolutePath(node: INode, path: string = ""): string { function getAbsolutePath(node: INode, path: string = ""): string {
if (node.parent === null) { let pathSeparator = "/";
return path ? `${node.name}/${path}` : node.name;
if (hostOSType === OperatingSystemType.WINDOWS) {
pathSeparator = "\\";
if (path.startsWith(pathSeparator)) path = path.substring(1);
} }
path = path.replace(`${pathSeparator}${pathSeparator}`, pathSeparator);
if (node.parent === null) {
if (hostOSType === OperatingSystemType.WINDOWS) return path ? path : node.name
return path ? `${node.name}${pathSeparator}${path}` : node.name;
}
const parentNode = flattenedFileTree.find(n => n.id === node.parent); const parentNode = flattenedFileTree.find(n => n.id === node.parent);
if (!parentNode) { if (!parentNode) {
throw new Error(`Parent node with id ${node.parent} not found`); throw new Error(`Parent node with id ${node.parent} not found`);
} }
return getAbsolutePath(parentNode, `${node.name}/${path}`); return getAbsolutePath(parentNode, `${node.name}${pathSeparator}${path}`);
} }
async function onLoadData({element}: { element: INode }) { async function onLoadData({element}: { element: INode }) {
@@ -54,14 +73,13 @@ export default function FileTreeView({setSelectedPath}: { setSelectedPath: (file
let subDirectories = await FilesystemEndpoint.listSubDirectories(absolutePath); let subDirectories = await FilesystemEndpoint.listSubDirectories(absolutePath);
if (subDirectories === undefined) return; if (subDirectories === undefined) return;
subDirectories = subDirectories.filter(r => r !== undefined);
const newNodes = fileDtosToNodes(subDirectories as FileDto[]); const newNodes = fileDtosToNodes(subDirectories as FileDto[]);
const updatedTree = updateTreeWithNewNodes(fileTree!!, element.id, newNodes); const updatedTree = updateTreeWithNewNodes(fileTree!!, element.id, newNodes);
setFileTree(updatedTree); setFileTree(updatedTree);
setFlattenedFileTree(flattenTree(updatedTree)); setFlattenedFileTree(flattenTree(updatedTree));
setSelectedPath(absolutePath); onPathChange(absolutePath);
} }
function updateTreeWithNewNodes(tree: ITreeNode, nodeId: NodeId, newNodes: ITreeNode[]): ITreeNode { function updateTreeWithNewNodes(tree: ITreeNode, nodeId: NodeId, newNodes: ITreeNode[]): ITreeNode {
@@ -74,7 +74,7 @@ export default function LibraryCreationModal({
/> />
</div> </div>
<pre>{selectedPath}</pre> <pre>{selectedPath}</pre>
<FileTreeView setSelectedPath={setSelectedPath}/> <FileTreeView onPathChange={setSelectedPath}/>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="light" onPress={onClose}> <Button variant="light" onPress={onClose}>
@@ -75,7 +75,7 @@ export default function TestView() {
{game && <GameOverviewCard game={game}></GameOverviewCard>} {game && <GameOverviewCard game={game}></GameOverviewCard>}
{game && <>{JSON.stringify(game, null, 2)}</>} {game && <>{JSON.stringify(game, null, 2)}</>}
<pre>{selectedPath}</pre> <pre>{selectedPath}</pre>
<FileTreeView setSelectedPath={setSelectedPath}/> <FileTreeView onPathChange={setSelectedPath}/>
</div> </div>
</div> </div>
); );
@@ -8,7 +8,9 @@ import jakarta.annotation.security.RolesAllowed
@RolesAllowed(Role.Names.ADMIN) @RolesAllowed(Role.Names.ADMIN)
class FilesystemEndpoint { class FilesystemEndpoint {
fun listContents(path: String?) = FilesystemService().listContents(path) fun listContents(path: String) = FilesystemService().listContents(path)
fun listSubDirectories(path: String?) = FilesystemService().listSubDirectories(path) fun listSubDirectories(path: String) = FilesystemService().listSubDirectories(path)
fun getHostOperatingSystem() = FilesystemService().getHostOperatingSystem()
} }
@@ -1,10 +1,17 @@
package de.grimsi.gameyfin.core.filesystem package de.grimsi.gameyfin.core.filesystem
import io.github.oshai.kotlinlogging.KotlinLogging
import org.apache.commons.io.FilenameUtils
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.File import java.nio.file.FileSystems
import kotlin.io.path.Path
import kotlin.io.path.isDirectory
@Service @Service
class FilesystemService { class FilesystemService {
private val log = KotlinLogging.logger {}
/** /**
* Lists all files and directories in the given path. * Lists all files and directories in the given path.
* If the path is null or empty, it lists all root directories. * If the path is null or empty, it lists all root directories.
@@ -13,12 +20,48 @@ class FilesystemService {
* @return A list of FileDto objects representing the files and directories. * @return A list of FileDto objects representing the files and directories.
*/ */
fun listContents(path: String?): List<FileDto> { fun listContents(path: String?): List<FileDto> {
val file = if (path.isNullOrEmpty()) File.listRoots().toList() else listOf(File(path)) if (path == null || path.isEmpty()) {
return file.flatMap { it.listFiles()?.toList() ?: emptyList() } return FileSystems.getDefault().rootDirectories
.map { FileDto(it.name, if (it.isDirectory) FileType.DIRECTORY else FileType.FILE, it.hashCode()) } .map {
FileDto(
it.root.toString(),
if (it.isDirectory()) FileType.DIRECTORY else FileType.FILE,
it.hashCode()
)
}
}
var path = FilenameUtils.separatorsToSystem(path)
if (path.startsWith("\\")) path = path.substring(1)
return try {
Path(path).toFile().listFiles()
.filter { f -> !f.isHidden }
.map { FileDto(it.name, if (it.isDirectory) FileType.DIRECTORY else FileType.FILE, it.hashCode()) }
} catch (_: Exception) {
log.error { "Error listing contents of path $path" }
emptyList()
}
} }
/**
* Lists all subdirectories in the given path.
* If the path is null or empty, it lists all root directories.
*
* @param path The path to list subdirectories from.
* @return A list of FileDto objects representing the subdirectories.
*/
fun listSubDirectories(path: String?): List<FileDto> { fun listSubDirectories(path: String?): List<FileDto> {
return listContents(path).filter { it.type == FileType.DIRECTORY } return listContents(path).filter { it.type == FileType.DIRECTORY }
} }
fun getHostOperatingSystem(): OperatingSystemType {
val os = System.getProperty("os.name").lowercase()
return when {
os.contains("win") -> OperatingSystemType.WINDOWS
os.contains("mac") -> OperatingSystemType.MAC
os.contains("nux") -> OperatingSystemType.LINUX
else -> OperatingSystemType.UNKNOWN
}
}
} }
@@ -0,0 +1,8 @@
package de.grimsi.gameyfin.core.filesystem
enum class OperatingSystemType {
WINDOWS,
MAC,
LINUX,
UNKNOWN,
}