mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 16:20:03 +00:00
Fix for multi-platform compatibility (Windows FS sucks)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import jakarta.annotation.security.RolesAllowed
|
|||||||
@Endpoint
|
@Endpoint
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
class FilesystemEndpoint {
|
class FilesystemEndpoint {
|
||||||
|
|
||||||
fun listContents(path: String?) = FilesystemService().listContents(path)
|
|
||||||
|
|
||||||
fun listSubDirectories(path: String?) = FilesystemService().listSubDirectories(path)
|
fun listContents(path: String) = FilesystemService().listContents(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,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user