FileService
Read, write, list, copy, move and recycle files and folders, open a file for ranged work, and follow one that grows.
"readings.json"GetPathHandler(path)ReadStringAsync()nil, whyA service (see Services): call its members with a colon. In a .shot file:
use FileServiceIn plain Luau:
local FileService = require("@shotsystem/FileService")Most work goes through a PathHandler, an instance for one path: GetPathHandler(path).
- A relative path starts at the project file's folder, wherever you run
shot run. .Pathis the path as given, backslashes turned into/: "box", a backslash and "todo.txt" read back asbox/todo.txt.
FileService:GetPathHandler("."):Join("box").Path ./box- A mistake in the code is an error: an empty path or name, an index out of range, a target that is no PathHandler, a destroyed handler, a member called with a dot.
- On a failure of the world, every disk
...Asyncmember returnsnil(orfalse) and why; ExistsAsync, IsFileAsync, IsFolderAsync and GetFileTypeAsync just answer. A success printstrue nil.
Each example is Main.shot of a one-file project, Demo.ssp (see Services), and checks with 0 errors.
Paths and handlers
Naming, no disk:
Path: string, BaseName: string | the path as given, and its last name |
Join(...: string) -> PathHandler | a path below this one |
Connect(name) -> string | the same, as a string |
GetParent() -> PathHandler? | nil for a bare name like "readings.json": use GetPathHandler(". |
GetNameAndExtension() -> (string, string?) | "todo.txt" gives todo, txt |
Asking:
ExistsAsync(), IsFileAsync(), IsFolderAsync() -> boolean
GetFileTypeAsync() -> "File" | "Folder" | "None"Reading and writing whole files:
ReadStringAsync() -> (string?, string?)
ReadBufferAsync() -> (buffer?, string?)
OverwriteAsync(content: string | buffer) -> (boolean, string?) replaces the file, making the folders above it
WriteChildAsync(name, content) -> (boolean, string?) the same for a file in this folderFolders:
ListDirAsync() -> ({ [string]: PathHandler }?, string?) name to PathHandler, in no order: sort the names
FindFirstChildAsync(name) -> (PathHandler?, string?) nil, nil when there is no such child
CreateSubFolderAsync(name), CreateSubFileAsync(name) -> (PathHandler?, string?) refused when it existsMoving and copying (the target is a folder's PathHandler):
CopyAsync(targetParent, newName?) -> (PathHandler?, string?) a folder copies with everything in it
MoveIntoAsync(targetParent, newName?) -> (PathHandler?, string?)Recycling and opening (below):
RecycleAsync() -> (RecycledItem?, string?)
OpenAsync() -> (FileHandler?, string?)A PathHandler has no Parent and no children; Instance.new can't make one.
use FileService
local here = FileService:GetPathHandler(".")
local box, why = here:CreateSubFolderAsync("box")
print("made box:", box ~= nil, why)
print("again:", here:CreateSubFolderAsync("box"))
if not box then
return
end
local todo = box:Join("todo.txt")
print(todo.Path, todo.BaseName, todo:GetFileTypeAsync())
print(todo:OverwriteAsync("buy milk\n"))
print(box:WriteChildAsync("done.txt", "call Sam\n"))
print(todo:GetFileTypeAsync(), box:GetFileTypeAsync())
print(todo:ReadStringAsync())
print("parent of a bare name:", FileService:GetPathHandler("readings.json"):GetParent())
print("not found:", box:FindFirstChildAsync("nope.txt"))
local list, listWhy = box:ListDirAsync()
if list then
local names = {}
for name in list do
table.insert(names, name)
end
table.sort(names)
print("list:", table.concat(names, ", "))
else
print("list failed:", listWhy)
endprints:
made box: true nil
again: nil CreateSubFolderAsync ./box: it exists already
./box/todo.txt todo.txt None
true nil
true nil
File Folder
buy milk
nil
parent of a bare name: nil
not found: nil nil
list: done.txt, todo.txtThe text's line end puts the reason's nil on the next line.
Copying and moving:
use FileService
local here = FileService:GetPathHandler(".")
local a = assert(here:CreateSubFolderAsync("a"))
local b = assert(here:CreateSubFolderAsync("b"))
assert(a:WriteChildAsync("note.txt", "hello"))
local note = a:Join("note.txt")
print("copy:", (assert(note:CopyAsync(b))).Path)
print("copy again:", note:CopyAsync(b))
local second = assert(note:CopyAsync(b, "note2.txt"))
print("renamed copy:", second.Path)
local moved = assert(note:MoveIntoAsync(b, "moved.txt"))
print("moved:", moved.Path, note:ExistsAsync())
print("into a file:", moved:CopyAsync(second))
print("missing:", note:CopyAsync(b))
local c = assert(b:CopyAsync(here, "c"))
local names = {}
for name in assert(c:ListDirAsync()) do
table.insert(names, name)
end
table.sort(names)
print("folder copy:", c.Path, table.concat(names, ", "))prints:
copy: ./b/note.txt
copy again: nil CopyAsync ./a/note.txt: ./b/note.txt exists already
renamed copy: ./b/note2.txt
moved: ./b/moved.txt false
into a file: nil CopyAsync ./b/moved.txt: ./b/note2.txt is no folder
missing: nil CopyAsync ./a/note.txt: no such file or folder
folder copy: ./c moved.txt, note.txt, note2.txtFiles open for ranged work
PathHandler:OpenAsync() -> (FileHandler?, string?) opens a file (a missing one is made empty; a folder is refused). The FileHandler is a child of FileService:
Length: number the size as this handler last saw it
ReadStringAsync(start, finish) -> (string?, string?) bytes start to finish, 1-based and inclusive, as string.sub
ReadBufferAsync(start, finish) -> (buffer?, string?)
WriteAsync(content, index) -> (boolean, string?) inserts at index (1 to Length + 1); nothing is overwritten
AppendAsync(content) -> (boolean, string?)Example:
use FileService
local note = FileService:GetPathHandler("note.txt")
note:OverwriteAsync("hello")
local handle, why = note:OpenAsync()
if not handle then
error("couldn't open: " .. tostring(why))
end
print(handle.ClassName, handle.Name, handle.Parent == FileService, handle.Length)
print(handle:ReadStringAsync(2, 4))
print(handle:ReadStringAsync(4, 99))
print(handle:WriteAsync(">", 1))
print(handle:AppendAsync("!"))
print(handle.Length, note:ReadStringAsync())
print("bad index:", pcall(function()
return handle:WriteAsync("x", 99)
end))
print("bad start:", pcall(function()
return handle:ReadStringAsync(0, 2)
end))
note:OverwriteAsync("hi")
print("after an outside write:", handle.Length)
print(handle:WriteAsync("x", 7))
print(handle:ReadStringAsync(1, 10), handle.Length)
print("open a folder:", FileService:GetPathHandler("."):OpenAsync())
local fresh, freshWhy = FileService:GetPathHandler("new.txt"):OpenAsync()
print("open a missing file:", if fresh then fresh.Length else "nil", freshWhy, FileService:GetPathHandler("new.txt"):ExistsAsync())
handle:Destroy()
print("after Destroy:", pcall(function()
return handle:ReadStringAsync(1, 1)
end))prints:
FileHandler note.txt true 5
ell nil
lo nil
true nil
true nil
7 >hello! nil
bad index: false Main.shot:16: WriteAsync: index is a whole number from 1 to Length + 1 (8), got 99
bad start: false Main.shot:19: ReadStringAsync: start is a whole number from 1, got 0
after an outside write: 7
false WriteAsync note.txt: the file has 2 bytes now, fewer than index - 1 (6)
hi 2
open a folder: nil OpenAsync .: it is a folder
open a missing file: 0 nil true
after Destroy: false Main.shot:30: FileHandler "note.txt" was destroyed- A range past the end gives what is there (
lo). - Length updates only when this handler reads or writes: after the outside write it still said 7, the insert was refused, and the next read set it to 2.
- Destroy the handler when done.
The service's own calls
For a single call, FileService takes path strings:
ReadAsync(path) -> (string?, string?) WriteAsync(path, text) -> (boolean, string?)
AppendAsync(path, text) -> (boolean, string?) SizeAsync(path) -> (number?, string?)
ExistsAsync(path) -> boolean IsFolderAsync(path) -> boolean
ListAsync(path) -> ({ FileEntry }?, string?) MakeFolderAsync(path) -> (boolean, string?)
CopyAsync(from, to, overwrite?) -> (boolean, string?) MoveAsync(from, to, overwrite?) -> (boolean, string?)
RemoveAsync(path) -> (boolean, string?) ReadFromAsync(path, offset) -> (string?, string?)- WriteAsync and AppendAsync make missing folders; AppendAsync makes a missing file.
- ListAsync's entries are
{ Name, IsFolder, Modified }, sorted by name; Modified is seconds since 1970, or nil. - ReadFromAsync gives the text after offset bytes; past the end:
nil ReadFromAsync notes/todo.txt: the file has 18 bytes, fewer than the offset 99- CopyAsync and MoveAsync refuse an existing target unless overwrite is true:
false CopyAsync notes: A directory already exists at the path 'notes2'- RemoveAsync deletes for good, a folder with everything in it; RecycleAsync can be undone.
Following a file
FileService:Follow(path, offset?, interval?) -> FileFollower follows a growing file, such as a log. It returns at once, a child of FileService, and looks every interval seconds (0.5 by default).
Textfires with each added part, in order (a part can hold several lines).Linefires with each whole line, without its line end; a half-written line waits.Resetfires when the file got shorter; reading starts over, so Text brings the new content.Offsetis how far it has read. An offset of 0 starts at the beginning, nil at the end (Offset then reads 0 until the first look).Destroy()ends it. Until then the program keeps running.- A negative offset is an error:
Follow: offset is a whole number of bytes from 0 or nil, got -1Example:
use FileService
use TimeService
local log = FileService:GetPathHandler("app.log")
log:OverwriteAsync("")
local follow = FileService:Follow("app.log", 0, 0.1)
print(follow.ClassName, follow.Name, follow.Parent == FileService, follow.Path)
follow.Text:Connect(function(added : string)
print("added: [" .. added .. "] offset " .. follow.Offset)
end)
follow.Reset:Connect(function()
print("the file got shorter")
end)
for step = 1, 3 do
FileService:AppendAsync("app.log", "step " .. step .. "\n")
TimeService:WaitAsync(0.3)
end
FileService:AppendAsync("app.log", "a\nb\n")
TimeService:WaitAsync(0.3)
log:OverwriteAsync("x\n")
TimeService:WaitAsync(0.3)
print("offset before Destroy " .. follow.Offset)
follow:Destroy()
FileService:AppendAsync("app.log", "after\n")
TimeService:WaitAsync(0.3)
print("done")prints:
FileFollower app.log true app.log
added: [step 1
] offset 7
added: [step 2
] offset 14
added: [step 3
] offset 21
added: [a
b
] offset 25
the file got shorter
added: [x
] offset 2
offset before Destroy 2
doneLine, with a line written in two parts:
use FileService
use TimeService
FileService:WriteAsync("app.log", "")
local follow = FileService:Follow("app.log", 0)
follow.Line:Connect(function(line : string)
print("line: [" .. line .. "] offset " .. follow.Offset)
end)
follow.Text:Connect(function(added : string)
print("text: " .. #added .. " bytes")
end)
FileService:AppendAsync("app.log", "a\nb\nhalf")
TimeService:WaitAsync(0.8)
FileService:AppendAsync("app.log", " done\n")
TimeService:WaitAsync(0.8)
follow:Destroy()prints:
line: [a] offset 2
line: [b] offset 4
text: 8 bytes
line: [half done] offset 14
text: 6 bytesEach look reads the whole file again.
The recycle bin
PathHandler:RecycleAsync() -> (RecycledItem?, string?) moves a file or folder into .ShotRecycleBin at its drive's root. A RecycledItem has OriginalPath (as given), Path (its PathHandler in the bin), RecycledAt (seconds since 1970) and:
RestoreAsync() -> (PathHandler?, string?) | back to OriginalPath; refused when something is there now |
DeleteAsync() -> (boolean, string?) | deletes it for good |
Example:
use FileService
local note = FileService:GetPathHandler("draft.txt")
note:OverwriteAsync("first draft")
local item, why = note:RecycleAsync()
if not item then
error("couldn't recycle: " .. tostring(why))
end
print(item.ClassName, item.Name, item.OriginalPath, item.Path ~= nil, item.Parent)
print(note:ExistsAsync(), item.RecycledAt > 0)
local back, backWhy = item:RestoreAsync()
print("restore:", if back then back.Path else "nil", backWhy, note:ReadStringAsync())
local second = note:RecycleAsync()
note:OverwriteAsync("second draft")
if second then
print("restore onto a file:", second:RestoreAsync())
print("delete:", second:DeleteAsync())
end
print("recycle nothing:", FileService:GetPathHandler("nothing.txt"):RecycleAsync())prints:
RecycledItem draft.txt draft.txt true nil
false true
restore: draft.txt nil first draft nil
restore onto a file: nil RestoreAsync draft.txt: something is there now
delete: true nil
recycle nothing: nil RecycleAsync nothing.txt: no such file or folderOne queue per path
Calls on one path take turns, in order, so writers started at once keep each other's text. Another case or the other slash is the same path.
use FileService
FileService:WriteAsync("count.log", "")
for n = 1, 50 do
task.spawn(function()
FileService:AppendAsync("count.log", n .. "\n")
end)
end
local text = FileService:ReadAsync("count.log") or ""
local lines = string.split(text, "\n")
print(#lines - 1, lines[1], lines[2], lines[50])prints 50 1 2 50: all fifty lines in order; the read waited its turn.
Mistakes
A dot call (Services, REACH A SERVICE) and Instance.new (Instances, MAKING INSTANCES):
use FileService
print(pcall(function()
return FileService.ReadAsync("notes.txt")
end))
print(pcall(function()
return FileService.GetPathHandler("notes.txt")
end))
print(pcall(function()
return Instance.new("PathHandler")
end))shot check --project Demo.ssp prints:
Demo.ssp/Main.shot:4:9 TypeError: This function must be called with self. Did you mean to use a colon instead of a dot?
Demo.ssp/Main.shot:7:9 TypeError: This function must be called with self. Did you mean to use a colon instead of a dot?
Demo.ssp/Main.shot:10:9 PathHandler can't be made with Instance.new: FileService:GetPathHandler returns one
3 errors in 1 file (1 files checked, 0.4 s)and each errors at run time:
false Main.shot:4: FileService.ReadAsync is a method: call it with a colon, FileService:ReadAsync(...)
false Main.shot:7: FileService.GetPathHandler is a method: call it with a colon, FileService:GetPathHandler(...)
false Main.shot:10: PathHandler can't be made with Instance.new: FileService:GetPathHandler(path) gives oneFailures of the world, not errors:
missing:ReadStringAsync() | nil ReadStringAsync missing.txt: no such file |
here:ReadStringAsync() | nil ReadStringAsync .: access denied |
here:OverwriteAsync("x") | false OverwriteAsync .: access denied |
nowhere:ListDirAsync() | nil ListDirAsync nowhere: no such folder |
shot check refuses a value that may be nil used as text or a table:
use FileService
local todo = FileService:GetPathHandler("box/todo.txt")
print(todo:GetParent().Path)gives
Demo.ssp/Main.shot:4:7 TypeError: Value of type 'PathHandler?' could be nilAn empty path is an error:
GetPathHandler takes a path (a non-empty string), got string