File Handling
A scripting library does not only contain reading and writing operations, that is why we also include methods for manipulating files such as creations, deletions, permissions and others.
Create a file
You may want to create a file without immediately writing on it. For example, when you want to modify the permissions first or let another process/fiber handle it instead. For such and more reasons, you can create a file or directory using the createFile and createDirectory functions.
It is generally possible to create empty files and directories using the createFile and createDirectory functions:
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Creating extends IOApp.Simple:
val filePath = Path("path/to/your/desired/creation/NiceScript.scala")
def run: IO[Unit] =
for
_ <- filePath.createFile
created <- filePath.exists
_ <- IO.println(s"File created? $created")
yield ()
end Creating
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Creating extends IOApp.Simple:
val filePath = Path("path/to/your/desired/creation/NiceScript.scala")
def run: IO[Unit] =
for
_ <- Catscript.createFile(filePath)
created <- Catscript.exists(filePath)
_ <- IO.println(s"File created? $created")
yield ()
end Creating
Here, we are first creating the file using the createFile method and then checking its existent with the exists method.
Important: The createFile and createDirectory methods will only work if the parent directory already exists, and fail otherwise. The createDirectories method will recursively create the directories instead:
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Creating extends IOApp.Simple:
val emptyDirectories = Path("create/me/first")
def run: IO[Unit] =
emptyDirectories.createDirectories >> Path("now_i_can_be_created.fs").createFile
end Creating
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Creating extends IOApp.Simple:
val emptyDirectories = Path("create/me/first")
def run: IO[Unit] =
Catscript.createDirectories(emptyDirectories) >>
Catscript.createFile(Path("now_i_can_be_created.fs"))
end Creating
Exercise
For this exercise, write a function that creates a file in a (deeply) nested directory. The directories should be created if they don't exist.
extension (path: Path) def createFileAndDirectories: IO[Unit] = ???
def createFileAndDirectories(path: Path): IO[Unit] = ???
Deleting here and there
Creating is just the first part. Because memory is not infinite, you may also want to delete a file on your system.
Deleting a file is as easy as using the delete method:
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val annoyingFile = Path("desktop/extend_your_car_warranty_now.ad")
def run: IO[Unit] =
for
exists <- annoyingFile.exists
_ <- if exists then annoyingFile.delete
else IO.unit
yield ()
end Deleting
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val annoyingFile = Path("desktop/extend_your_car_warranty_now.ad")
def run: IO[Unit] =
for
exists <- Catscript.exists(annoyingFile)
_ <- if exists then Catscript.delete(annoyingFile)
else IO.unit
yield ()
end Deleting
Note that we are first checking if the file exists before deleting it, this is because trying to delete a file that does not exist will result in an error. To avoid this error, you have two options. One is using the whenA combinator from Applicative importing cats.syntax.applicative.*:
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.syntax.applicative.* // You can instead import cats.syntax.all.* !
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val annoyingFile = Path("desktop/extend_your_car_warranty_now.ad")
def run: IO[Unit] =
for
exists <- annoyingFile.exists
_ <- annoyingFile.delete.whenA(exists)
yield ()
end Deleting
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.syntax.applicative.* // You can instead import cats.syntax.all.* !
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val annoyingFile = Path("desktop/extend_your_car_warranty_now.ad")
def run: IO[Unit] =
for
exists <- Catscript.exists(annoyingFile)
_ <- Catscript.delete(annoyingFile).whenA(exists)
yield ()
end Deleting
Or even better, use the convenience method deleteIfExists:
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val annoyingFile = Path("desktop/extend_your_car_warranty_now.ad")
def run: IO[Unit] =
for
deleted <- annoyingFile.deleteIfExists
_ <- IO.println(s"Are they reaching out? $deleted")
yield ()
end Deleting
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val annoyingFile = Path("desktop/extend_your_car_warranty_now.ad")
def run: IO[Unit] =
for
deleted <- Catscript.deleteIfExists(annoyingFile)
_ <- IO.println(s"Are they reaching out? $deleted")
yield ()
end Deleting
This will return a boolean indicating whether the file and directories have been deleted.
Finally, you may want to delete not one but multiple files and directories, here is when the deleteDirectorires comes handy, as it will delete all the files and directories recursively (similar to rm -r):
Before:
/My files
├── /non empty folder
│ ├── 3751c91b_2024-06-16_7.csv
│ ├── Screenshot 2024-06-16 210523.png
│ ├── /downloaded
│ │ ├── /on_internet
│ │ │ └── Unconfirmed 379466.crdownload
│ │ └── ubuntu-24.04-desktop-amd64.iso
│ └── /spark
│ ├── output0-part-r-00000.nodes
│ └── output1-part-r-00000.nodes
│
├── /dont delete
│
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val nonEmptyFolder = Path("downloads/non empty folder")
def run: IO[Unit] = nonEmptyFolder.deleteRecursively
end Deleting
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Deleting extends IOApp.Simple:
val nonEmptyFolder = Path("downloads/non empty folder")
def run: IO[Unit] = Catscript.deleteRecursively(nonEmptyFolder)
end Deleting
After:
/My files
├── /dont delete
│
Exercise
You are tired of people abusing the FTP server to upload enormous files, so you decide to implement a method that checks if a file exceeds a certain size, and if so, automatically delete it (hint: check the size method). The method must return true if the file has been deleted, false otherwise.
extension (path: Path) def deleteIfChubby(threshold: Long): IO[Boolean] = ???
def deleteIfChubby(path: Path, threshold: Long): IO[Boolean] = ???
Using temporary files
Maybe you do not want to manually delete a file after its use. This is where temporary files come in to play, as they are deleted automatically.
To create temporary files, you have two options, one is to make Cats Effect automatically handle their lifecycle with the withTempFile and withTempDirectory methods (useful when you want the files deleted right away), or, if you rather prefer the operating system to take hands in its lifecycle, you can use the createTempFile and createTempDirectory variants (suitable if you do not care if the files are deleted immediately).
The former takes as a parameter a function that describes how you want to use the file, like this:
import org.typelevel.catscript.syntax.path.*
import cats.syntax.all.*
import cats.effect.{IO, IOApp}
object Temporary extends IOApp.Simple:
def run: IO[Unit] = withTempFile: path =>
for
_ <- path.writeLines(LazyList.from('a').map(_.toChar.toString).take(26))
alphabet <- path.read
_ <- IO.println("ASCII took hispanics into account!").whenA(alphabet.contains('ñ'))
yield ()
end Temporary
import org.typelevel.catscript.Catscript
import cats.syntax.all.*
import cats.effect.{IO, IOApp}
object Temporary extends IOApp.Simple:
def run: IO[Unit] = Catscript.withTempFile: path =>
for
_ <- Catscript.writeLines(path, LazyList.from('a').map(_.toChar.toString).take(26))
alphabet <- Catscript.read(path)
_ <- IO.println("ASCII took hispanics into account!").whenA(alphabet.contains('ñ'))
yield ()
end Temporary
You will see that the use function goes from Path => IO[A], and that use basically describes a path that will be used to compute an A, with some side effects along the way. When the computation is finished, the file will no longer exist.
The last alternative is with createTempFile or createTempDirectory. The difference between createTempFile and withTempFile is that the create functions return the path of the file, for example:
import org.typelevel.catscript.syntax.path.*
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Temporary extends IOApp.Simple:
val secretPath = Path(".secrets/to_my_secret_lover.txt")
def run: IO[Unit] = createTempFile.flatMap: path =>
for
_ <- path.write("A confession to my lover: ")
letter <- secretPath.read
_ <- path.appendLine(letter)
yield ()
end Temporary
import org.typelevel.catscript.Catscript
import fs2.io.file.Path
import cats.effect.{IO, IOApp}
object Temporary extends IOApp.Simple:
val secretPath = Path(".secrets/to_my_secret_lover.txt")
def run: IO[Unit] = Catscript.createTempFile.flatMap: path =>
for
_ <- Catscript.write(path,"A confession to my lover: ")
letter <- Catscript.read(secretPath)
_ <- Catscript.appendLine(path, letter)
yield ()
end Temporary
Exercise
Another really nice way to handle resource lifecycle is with a Resource from Cats Effect. Be adventurous and implement a third way of handling temporary files with a new function that returns a Resource.
import cats.effect.Resource
def makeTempFile: Resource[IO, Path] = ???
def makeTempDirectory: Resource[IO, Path] = ???