diff --git a/lib/pinchflat/boot/pre_job_startup_tasks.ex b/lib/pinchflat/boot/pre_job_startup_tasks.ex index 73847b9..202a746 100644 --- a/lib/pinchflat/boot/pre_job_startup_tasks.ex +++ b/lib/pinchflat/boot/pre_job_startup_tasks.ex @@ -14,6 +14,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do alias Pinchflat.Repo alias Pinchflat.Settings + alias Pinchflat.Filesystem.FilesystemHelpers def start_link(opts \\ []) do GenServer.start_link(__MODULE__, %{}, opts) @@ -31,6 +32,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do @impl true def init(state) do apply_default_settings() + ensure_directories_are_writeable() rename_old_job_workers() {:ok, state} @@ -41,6 +43,21 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do Settings.fetch!(:pro_enabled, false) end + defp ensure_directories_are_writeable do + directories = [ + Application.get_env(:pinchflat, :media_directory), + Application.get_env(:pinchflat, :tmpfile_directory), + Application.get_env(:pinchflat, :metadata_directory) + ] + + Enum.each(directories, fn dir -> + file = Path.join([dir, ".keep"]) + + # This will fail if the directory is not writeable, stopping boot + FilesystemHelpers.write_p!(file, "") + end) + end + # As part of a large refactor, I ended up moving a bunch of workers around. This # is a problem because the workers are stored in the database and the runner # will try to run the OLD jobs. This is also why these tasks run before the job diff --git a/lib/pinchflat/filesystem/filesystem_helpers.ex b/lib/pinchflat/filesystem/filesystem_helpers.ex index 528e107..895a8a6 100644 --- a/lib/pinchflat/filesystem/filesystem_helpers.ex +++ b/lib/pinchflat/filesystem/filesystem_helpers.ex @@ -48,4 +48,36 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do err end end + + @doc """ + Deletes a file and removes any empty directories in the path. + Does NOT remove any directories that are not empty. + + Returns :ok | {:error, any()} + """ + def delete_file_and_remove_empty_directories(filepath) do + case File.rm(filepath) do + :ok -> + filepath + |> Path.dirname() + |> recursively_delete_empty_directories() + + err -> + err + end + end + + defp recursively_delete_empty_directories(directory) do + case File.rmdir(directory) do + :ok -> + directory + |> Path.dirname() + |> recursively_delete_empty_directories() + + err -> + err + end + + :ok + end end diff --git a/lib/pinchflat/media/media.ex b/lib/pinchflat/media/media.ex index 500866f..74a4414 100644 --- a/lib/pinchflat/media/media.ex +++ b/lib/pinchflat/media/media.ex @@ -7,9 +7,10 @@ defmodule Pinchflat.Media do alias Pinchflat.Repo alias Pinchflat.Tasks - alias Pinchflat.Media.MediaItem alias Pinchflat.Sources.Source + alias Pinchflat.Media.MediaItem alias Pinchflat.Metadata.MediaMetadata + alias Pinchflat.Filesystem.FilesystemHelpers @doc """ Returns the list of media_items. @@ -254,16 +255,7 @@ defmodule Pinchflat.Media do media_item |> media_filepaths() |> Enum.concat(metadata_filepaths(media_item)) - |> Enum.each(&File.rm/1) - - # rmdir will attempt to delete the directory, but only if it is empty - if media_item.media_filepath do - File.rmdir(Path.dirname(media_item.media_filepath)) - end - - if media_item.metadata && media_item.metadata.metadata_filepath do - File.rmdir(Path.dirname(media_item.metadata.metadata_filepath)) - end + |> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/1) {:ok, media_item} end diff --git a/lib/pinchflat/sources/sources.ex b/lib/pinchflat/sources/sources.ex index 974a94d..ab85257 100644 --- a/lib/pinchflat/sources/sources.ex +++ b/lib/pinchflat/sources/sources.ex @@ -12,6 +12,7 @@ defmodule Pinchflat.Sources do alias Pinchflat.Profiles.MediaProfile alias Pinchflat.YtDlp.MediaCollection alias Pinchflat.Metadata.SourceMetadata + alias Pinchflat.Filesystem.FilesystemHelpers alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.FastIndexing.FastIndexingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers @@ -108,8 +109,8 @@ defmodule Pinchflat.Sources do Media.delete_media_item(media_item, delete_files: delete_files) end) - Tasks.delete_tasks_for(source) delete_source_metadata_files(source) + Tasks.delete_tasks_for(source) Repo.delete(source) end @@ -149,7 +150,7 @@ defmodule Pinchflat.Sources do |> Enum.map(fn field -> mapped_struct[field] end) |> Enum.filter(&is_binary/1) - Enum.each(filepaths, &File.rm/1) + Enum.each(filepaths, &FilesystemHelpers.delete_file_and_remove_empty_directories/1) end defp add_source_details_to_changeset(source, changeset) do diff --git a/test/pinchflat/filesystem/filesystem_helpers_test.exs b/test/pinchflat/filesystem/filesystem_helpers_test.exs index d776632..d5aef98 100644 --- a/test/pinchflat/filesystem/filesystem_helpers_test.exs +++ b/test/pinchflat/filesystem/filesystem_helpers_test.exs @@ -56,4 +56,55 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do File.rm!(filepath) end end + + describe "delete_file_and_remove_empty_directories/1" do + test "deletes file at the provided filepath" do + filepath = FilesystemHelpers.generate_metadata_tmpfile(:json) + + assert File.exists?(filepath) + + assert :ok = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath) + + refute File.exists?(filepath) + end + + test "deletes empty directories" do + tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory) + filepath = Path.join([tmpfile_directory, "foo", "bar", "baz", "qux.json"]) + FilesystemHelpers.write_p!(filepath, "") + + assert :ok = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath) + + refute File.exists?(filepath) + refute File.exists?(Path.join([tmpfile_directory, "foo", "bar", "baz"])) + refute File.exists?(Path.join([tmpfile_directory, "foo", "bar"])) + refute File.exists?(Path.join([tmpfile_directory, "foo"])) + end + + test "does not delete directories with other files in them" do + tmpfile_directory = Application.get_env(:pinchflat, :tmpfile_directory) + filepath_1 = Path.join([tmpfile_directory, "foo", "bar", "baz", "qux.json"]) + filepath_2 = Path.join([tmpfile_directory, "foo", "baz.json"]) + FilesystemHelpers.write_p!(filepath_1, "") + FilesystemHelpers.write_p!(filepath_2, "") + + assert :ok = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath_1) + + refute File.exists?(filepath_1) + refute File.exists?(Path.join([tmpfile_directory, "foo", "bar", "baz"])) + refute File.exists?(Path.join([tmpfile_directory, "foo", "bar"])) + + assert File.exists?(filepath_2) + assert File.exists?(Path.join([tmpfile_directory, "foo"])) + + # cleanup + FilesystemHelpers.delete_file_and_remove_empty_directories(filepath_2) + end + + test "returns an error if file could not be deleted" do + filepath = "/nonexistent/file.json" + + assert {:error, _} = FilesystemHelpers.delete_file_and_remove_empty_directories(filepath) + end + end end