Adds method to improve cleanup of empty directories

This commit is contained in:
Kieran Eglin 2024-03-14 15:01:04 -07:00
parent 0f3329e97d
commit 0de5ea901d
No known key found for this signature in database
GPG key ID: 193984967FCF432D
5 changed files with 106 additions and 13 deletions

View file

@ -14,6 +14,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Settings alias Pinchflat.Settings
alias Pinchflat.Filesystem.FilesystemHelpers
def start_link(opts \\ []) do def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, opts) GenServer.start_link(__MODULE__, %{}, opts)
@ -31,6 +32,7 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
@impl true @impl true
def init(state) do def init(state) do
apply_default_settings() apply_default_settings()
ensure_directories_are_writeable()
rename_old_job_workers() rename_old_job_workers()
{:ok, state} {:ok, state}
@ -41,6 +43,21 @@ defmodule Pinchflat.Boot.PreJobStartupTasks do
Settings.fetch!(:pro_enabled, false) Settings.fetch!(:pro_enabled, false)
end 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 # 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 # 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 # will try to run the OLD jobs. This is also why these tasks run before the job

View file

@ -48,4 +48,36 @@ defmodule Pinchflat.Filesystem.FilesystemHelpers do
err err
end end
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 end

View file

@ -7,9 +7,10 @@ defmodule Pinchflat.Media do
alias Pinchflat.Repo alias Pinchflat.Repo
alias Pinchflat.Tasks alias Pinchflat.Tasks
alias Pinchflat.Media.MediaItem
alias Pinchflat.Sources.Source alias Pinchflat.Sources.Source
alias Pinchflat.Media.MediaItem
alias Pinchflat.Metadata.MediaMetadata alias Pinchflat.Metadata.MediaMetadata
alias Pinchflat.Filesystem.FilesystemHelpers
@doc """ @doc """
Returns the list of media_items. Returns the list of media_items.
@ -254,16 +255,7 @@ defmodule Pinchflat.Media do
media_item media_item
|> media_filepaths() |> media_filepaths()
|> Enum.concat(metadata_filepaths(media_item)) |> Enum.concat(metadata_filepaths(media_item))
|> Enum.each(&File.rm/1) |> Enum.each(&FilesystemHelpers.delete_file_and_remove_empty_directories/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
{:ok, media_item} {:ok, media_item}
end end

View file

@ -12,6 +12,7 @@ defmodule Pinchflat.Sources do
alias Pinchflat.Profiles.MediaProfile alias Pinchflat.Profiles.MediaProfile
alias Pinchflat.YtDlp.MediaCollection alias Pinchflat.YtDlp.MediaCollection
alias Pinchflat.Metadata.SourceMetadata alias Pinchflat.Metadata.SourceMetadata
alias Pinchflat.Filesystem.FilesystemHelpers
alias Pinchflat.Downloading.DownloadingHelpers alias Pinchflat.Downloading.DownloadingHelpers
alias Pinchflat.FastIndexing.FastIndexingHelpers alias Pinchflat.FastIndexing.FastIndexingHelpers
alias Pinchflat.SlowIndexing.SlowIndexingHelpers alias Pinchflat.SlowIndexing.SlowIndexingHelpers
@ -108,8 +109,8 @@ defmodule Pinchflat.Sources do
Media.delete_media_item(media_item, delete_files: delete_files) Media.delete_media_item(media_item, delete_files: delete_files)
end) end)
Tasks.delete_tasks_for(source)
delete_source_metadata_files(source) delete_source_metadata_files(source)
Tasks.delete_tasks_for(source)
Repo.delete(source) Repo.delete(source)
end end
@ -149,7 +150,7 @@ defmodule Pinchflat.Sources do
|> Enum.map(fn field -> mapped_struct[field] end) |> Enum.map(fn field -> mapped_struct[field] end)
|> Enum.filter(&is_binary/1) |> Enum.filter(&is_binary/1)
Enum.each(filepaths, &File.rm/1) Enum.each(filepaths, &FilesystemHelpers.delete_file_and_remove_empty_directories/1)
end end
defp add_source_details_to_changeset(source, changeset) do defp add_source_details_to_changeset(source, changeset) do

View file

@ -56,4 +56,55 @@ defmodule Pinchflat.Filesystem.FilesystemHelpersTest do
File.rm!(filepath) File.rm!(filepath)
end end
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 end