Module FileSystem
	EnableExplicit
	
	; Private constants
	#BlobsDir = "blobs/"
	
	;- Helpers
	Procedure CanAccess(UserID, NodeID)
		Protected OwnerID = Database::FSGetOwner(NodeID)
		ProcedureReturn Bool(OwnerID = UserID Or OwnerID = 0)
	EndProcedure
	
	Procedure ResolveFromQuery(*Request, UserID)
		Protected IDStr.s = General::GetQueryField(*Request, "id")
		If IDStr <> "" : ProcedureReturn Val(IDStr) : EndIf
		ProcedureReturn Database::FSResolve(UserID, General::GetQueryField(*Request, "path"))
	EndProcedure
	
	Procedure ResolveFromPost(*Request, UserID)
		Protected IDStr.s = General::GetPostField(*Request, "id")
		If IDStr <> "" : ProcedureReturn Val(IDStr) : EndIf
		ProcedureReturn Database::FSResolve(UserID, General::GetPostField(*Request, "path"))
	EndProcedure
	
	Procedure ResolveParent(UserID, Path.s)
		While Right(Path, 1) = "/" : Path = Left(Path, Len(Path) - 1) : Wend
		ProcedureReturn Database::FSResolve(UserID, Path)
	EndProcedure
	
	;- Public procedures
	
	; GET /api/fs/list?path=...  or  ?id=...
	Procedure HandleList(*Request)
		Protected UserID, NodeID
		Protected Username.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID = Database::FindUser(Username)
		NodeID = ResolveFromQuery(*Request, UserID)
		
		If NodeID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		If Not CanAccess(UserID, NodeID)
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		General::RespondJSON(*Request, Database::FSList(NodeID))
	EndProcedure
	
	; GET /api/fs/stat?path=...  or  ?id=...
	Procedure HandleStat(*Request)
		Protected UserID, NodeID
		Protected Username.s, Stat.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID = Database::FindUser(Username)
		NodeID = ResolveFromQuery(*Request, UserID)
		
		If NodeID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		If Not CanAccess(UserID, NodeID)
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		Stat = Database::FSStat(NodeID)
		If Stat = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Not found\"}", "404 Not Found")
		Else
			General::RespondJSON(*Request, Stat)
		EndIf
	EndProcedure
	
	; GET /api/fs/read?path=...  or  ?id=...
	Procedure HandleRead(*Request)
		Protected UserID, NodeID
		Protected IsDir.Integer
		Protected Username.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID = Database::FindUser(Username)
		NodeID = ResolveFromQuery(*Request, UserID)
		
		If NodeID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		If Not CanAccess(UserID, NodeID)
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		Database::FSGetOwner(NodeID, @IsDir)
		If IsDir\i
			General::RespondJSON(*Request, ~"{\"error\":\"Is a directory\"}", "400 Bad Request")
			ProcedureReturn
		EndIf
		
		If Not FastCGI::RespondFile(*Request, #BlobsDir + NodeID)
			General::RespondJSON(*Request, ~"{\"error\":\"Blob missing\"}", "404 Not Found")
		EndIf
	EndProcedure
	
	; POST /api/fs/write  body: path=...&content=...  [&mime_type=...]
	Procedure HandleWrite(*Request)
		Protected UserID, ParentID, NodeID, FileID, BufLen, Size, *Buf
		Protected Username.s, Path.s, Content.s, MimeType.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID   = Database::FindUser(Username)
		Path     = General::GetPostField(*Request, "path")
		Content  = General::GetPostField(*Request, "content")
		MimeType = General::GetPostField(*Request, "mime_type")
		
		If Path = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Missing path\"}")
			ProcedureReturn
		EndIf
		If MimeType = "" : MimeType = "text/plain" : EndIf
		
		ParentID = ResolveParent(UserID, GetPathPart(Path))
		If ParentID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Parent directory not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		If Not CanAccess(UserID, ParentID)
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		NodeID = Database::FSResolve(UserID, Path)
		If NodeID = 0
			NodeID = Database::FSCreateFile(UserID, ParentID, GetFilePart(Path), MimeType)
		EndIf
		If NodeID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Could not create node\"}", "500 Internal Server Error")
			ProcedureReturn
		EndIf
		
		FileID = CreateFile(#PB_Any, #BlobsDir + NodeID)
		If FileID
			BufLen = StringByteLength(Content, #PB_UTF8)
			If BufLen > 0
				*Buf = AllocateMemory(BufLen)
				If *Buf
					PokeS(*Buf, Content, -1, #PB_UTF8 | #PB_String_NoZero)
					WriteData(FileID, *Buf, BufLen)
					FreeMemory(*Buf)
				EndIf
			EndIf
			Size = Lof(FileID)
			CloseFile(FileID)
			Database::FSUpdateFile(NodeID, Size)
			General::RespondJSON(*Request, ~"{\"success\":true,\"id\":" + NodeID + "}")
		Else
			General::RespondJSON(*Request, ~"{\"error\":\"Could not write blob\"}", "500 Internal Server Error")
		EndIf
	EndProcedure
	
	; POST /api/fs/mkdir  body: path=...
	Procedure HandleMkdir(*Request)
		Protected UserID, ParentID, DirID
		Protected Username.s, Path.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID = Database::FindUser(Username)
		Path   = General::GetPostField(*Request, "path")
		
		If Path = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Missing path\"}")
			ProcedureReturn
		EndIf
		
		ParentID = ResolveParent(UserID, GetPathPart(Path))
		If ParentID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Parent not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		If Not CanAccess(UserID, ParentID)
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		DirID = Database::FSMkdir(UserID, ParentID, GetFilePart(Path))
		If DirID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Already exists or could not create\"}", "409 Conflict")
		Else
			General::RespondJSON(*Request, ~"{\"success\":true,\"id\":" + DirID + "}")
		EndIf
	EndProcedure
	
	; POST /api/fs/delete  body: path=...  or  id=...
	Procedure HandleDelete(*Request)
		Protected UserID, NodeID, OwnerID
		Protected IsDir.Integer
		Protected Username.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID = Database::FindUser(Username)
		NodeID = ResolveFromPost(*Request, UserID)
		
		If NodeID = 0
			General::RespondJSON(*Request, ~"{\"error\":\"Not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		
		OwnerID = Database::FSGetOwner(NodeID, @IsDir)
		If OwnerID <> UserID
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		If Not IsDir\i : DeleteFile(#BlobsDir + NodeID) : EndIf
		
		Database::FSDelete(NodeID)
		General::RespondJSON(*Request, ~"{\"success\":true}")
	EndProcedure
	
	; POST /api/fs/move  body: path=...&to=...  or  id=...&to_parent_id=...&name=...
	Procedure HandleMove(*Request)
		Protected UserID, NodeID, NewParentID, OwnerID
		Protected IsDir.Integer
		Protected Username.s, IDStr.s, NewName.s, FromPath.s, ToPath.s
		
		Username = Auth::GetSessionUser(*Request)
		If Username = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Unauthorized\"}", "401 Unauthorized")
			ProcedureReturn
		EndIf
		UserID = Database::FindUser(Username)
		IDStr  = General::GetPostField(*Request, "id")
		
		If IDStr <> ""
			NodeID      = Val(IDStr)
			NewParentID = Val(General::GetPostField(*Request, "to_parent_id"))
			NewName     = General::GetPostField(*Request, "name")
		Else
			FromPath    = General::GetPostField(*Request, "path")
			ToPath      = General::GetPostField(*Request, "to")
			NodeID      = Database::FSResolve(UserID, FromPath)
			NewParentID = ResolveParent(UserID, GetPathPart(ToPath))
			NewName     = GetFilePart(ToPath)
		EndIf
		
		If NodeID = 0 Or NewParentID = 0 Or NewName = ""
			General::RespondJSON(*Request, ~"{\"error\":\"Not found\"}", "404 Not Found")
			ProcedureReturn
		EndIf
		
		OwnerID = Database::FSGetOwner(NodeID, @IsDir)
		If OwnerID <> UserID
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		If Not CanAccess(UserID, NewParentID)
			General::RespondJSON(*Request, ~"{\"error\":\"Forbidden\"}", "403 Forbidden")
			ProcedureReturn
		EndIf
		
		Database::FSMove(NodeID, NewParentID, NewName)
		General::RespondJSON(*Request, ~"{\"success\":true}")
	EndProcedure
	
EndModule

; IDE Options = PureBasic 6.30 (Windows - x64)
; CursorPosition = 2
; Folding = BA-
; EnableXP
; DPIAware