diff --git a/cookbook/models.py b/cookbook/models.py index 04204fe9..2f002105 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -114,7 +114,8 @@ class UserPreference(models.Model): class Storage(models.Model): DROPBOX = 'DB' NEXTCLOUD = 'NEXTCLOUD' - STORAGE_TYPES = ((DROPBOX, 'Dropbox'), (NEXTCLOUD, 'Nextcloud')) + LOCAL = 'LOCAL' + STORAGE_TYPES = ((DROPBOX, 'Dropbox'), (NEXTCLOUD, 'Nextcloud'), (LOCAL, 'Local')) name = models.CharField(max_length=128) method = models.CharField( diff --git a/cookbook/provider/local.py b/cookbook/provider/local.py new file mode 100644 index 00000000..1ec79157 --- /dev/null +++ b/cookbook/provider/local.py @@ -0,0 +1,59 @@ +import io +import os +import tempfile +from datetime import datetime +from os import listdir +from os.path import isfile, join + +from cookbook.models import Recipe, RecipeImport, SyncLog +from cookbook.provider.provider import Provider + + +class Local(Provider): + + @staticmethod + def import_all(monitor): + files = [f for f in listdir(monitor.path) if isfile(join(monitor.path, f))] + + import_count = 0 + for file in files: + path = monitor.path + '/' + file + if not Recipe.objects.filter(file_path__iexact=path).exists() \ + and not RecipeImport.objects.filter(file_path=path).exists(): # noqa: E501 + name = os.path.splitext(file)[0] + new_recipe = RecipeImport( + name=name, + file_path=path, + storage=monitor.storage + ) + new_recipe.save() + import_count += 1 + + log_entry = SyncLog( + status='SUCCESS', + msg='Imported ' + str(import_count) + ' recipes', + sync=monitor + ) + log_entry.save() + + monitor.last_checked = datetime.now() + monitor.save() + + return True + + @staticmethod + def get_file(recipe): + file = io.BytesIO(open(recipe.file_path, 'rb').read()) + + return file + + @staticmethod + def rename_file(recipe, new_name): + os.rename(recipe.file_path, os.path.join(os.path.dirname(recipe.file_path), (new_name + os.path.splitext(recipe.file_path)[1]))) + + return True + + @staticmethod + def delete_file(recipe): + os.remove(recipe.file_path) + return True diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 739c340d..a29790cd 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -38,6 +38,7 @@ from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, Storage, Sync, SyncLog, Unit, UserPreference, ViewLog, RecipeBookEntry) from cookbook.provider.dropbox import Dropbox +from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud from cookbook.serializer import (FoodSerializer, IngredientSerializer, KeywordSerializer, MealPlanSerializer, @@ -370,6 +371,8 @@ def get_recipe_provider(recipe): return Dropbox elif recipe.storage.method == Storage.NEXTCLOUD: return Nextcloud + elif recipe.storage.method == Storage.LOCAL: + return Local else: raise Exception('Provider not implemented') @@ -394,8 +397,8 @@ def get_external_file_link(request, recipe_id): @group_required('user') def get_recipe_file(request, recipe_id): recipe = Recipe.objects.get(id=recipe_id) - if not recipe.cors_link: - update_recipe_links(recipe) + # if not recipe.cors_link: + # update_recipe_links(recipe) return FileResponse(get_recipe_provider(recipe).get_file(recipe)) @@ -420,6 +423,10 @@ def sync_all(request): ret = Nextcloud.import_all(monitor) if not ret: error = True + if monitor.storage.method == Storage.LOCAL: + ret = Local.import_all(monitor) + if not ret: + error = True if not error: messages.add_message( diff --git a/cookbook/views/delete.py b/cookbook/views/delete.py index a2142756..689ad2f1 100644 --- a/cookbook/views/delete.py +++ b/cookbook/views/delete.py @@ -13,6 +13,7 @@ from cookbook.models import (Comment, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, Storage, Sync) from cookbook.provider.dropbox import Dropbox +from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud @@ -37,6 +38,8 @@ def delete_recipe_source(request, pk): Dropbox.delete_file(recipe) if recipe.storage.method == Storage.NEXTCLOUD: Nextcloud.delete_file(recipe) + if recipe.storage.method == Storage.LOCAL: + Local.delete_file(recipe) recipe.storage = None recipe.file_path = '' diff --git a/cookbook/views/edit.py b/cookbook/views/edit.py index 17050d56..4259fc97 100644 --- a/cookbook/views/edit.py +++ b/cookbook/views/edit.py @@ -18,6 +18,7 @@ from cookbook.models import (Comment, Food, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeImport, Storage, Sync) from cookbook.provider.dropbox import Dropbox +from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud @@ -231,6 +232,8 @@ class ExternalRecipeUpdate(GroupRequiredMixin, UpdateView): Dropbox.rename_file(old_recipe, self.object.name) if self.object.storage.method == Storage.NEXTCLOUD: Nextcloud.rename_file(old_recipe, self.object.name) + if self.object.storage.method == Storage.LOCAL: + Local.rename_file(old_recipe, self.object.name) self.object.file_path = "%s/%s%s" % ( os.path.dirname(self.object.file_path), diff --git a/docs/features/external_recipes.md b/docs/features/external_recipes.md index 62ee01d4..481a04b3 100644 --- a/docs/features/external_recipes.md +++ b/docs/features/external_recipes.md @@ -12,9 +12,6 @@ Lastly you will need to sync with the external path and import recipes you desir ## Storage -!!! success - Currently only Nextcloud and Dropbox are supported. There are plans to add more provider - !!! danger In order for this application to retrieve data from external providers it needs to store authentication information. Please use read only/separate accounts or app passwords wherever possible. @@ -31,6 +28,24 @@ The basic configuration is the same for all providers. | Name | Your identifier for this storage source, can be everything you want. | | Method | The desired method. | +!!! success + Only the providers listed below are currently implemented. If you need anything else feel free to open + an issue or pull request. + +### Local + +!!! info + There is currently no way to upload files trough the webinterface. This is a feature that might be added later. + +The local provider does not need any configuration. +For the monitor you will need to define a valid path on your host system. +The Path depends on your setup and can be both relative and absoulte. +If you use docker the default directory is `/opt/recipes/` + +!!! warning "Volume" + By default no data other than the mediafiles and the database is persisted. If you use the local provider + make sure to mount the path you choose to monitor to your host system in order to keep it persistent. + ### Dropbox | Field | Value |