Usuwanie plików i download plików

Witam.

Mam 2 pytania bo nijak nie mogę sobie poradzić, przejrzałem dużo postów ale nie znalazłem odpowiedniego rozwiązania.

  1. W jaki sposób usunąć fizyczne plik z dysku wraz z usunięciem rekordu z bazy wykorzystując sposób jaki jest zawarty w standardowej procedurze usuwania rekordów w kontrolerze. W jednym polu tabeli mam nazwę tego pliku, np.: plik.zip

  2. W jaki sposób mogę wykonać download plików tak aby adres pobieranego pliku nie był widoczny wprost. Tak jak powyżej nazwa pliku jest także w polu tabeli.

Witaj,

  1. Klasa CActiveRecord ma metodę afterDelete(). Musisz w swoim modelu dodać tą metodę i w niej napisać to co ma się dziać po usunięciu rekordu z bazy.

  2. To jest chyba proste :slight_smile:

Masz tabelę "files". W rekordzie o Id=1 masz pole z nazwą pliku "plik.zip".

Tworzysz np. akcję "download" w kontrolerze "FilesController", która przyjmuje parametr $id.

Wtedy sprawdzasz czy w bazie jest taki rekord, odczytujesz nazwę pliku i przesyłasz jego zawartość do przeglądarki (musisz dać też odpowiedni header, aby otworzyło się okno do zapisu pliku - sprawdź przykład na tej stronie:

http://php.net/manual/en/function.readfile.php)

Pozdrawiam Mariusz

Co do odpowiedzi na drugie pytanie: readfile() to raczej zły pomysł. Może się sprawdzić dla jakichś małych plików (zdjęcia, pliki tekstowe) jednak przy większych plikach mogą być problemy z wydajnością i duże zużycie ramu.

Lepiej pomyśleć o wykorzystaniu modułu do Apache - X-Sendfile. Minimalnie obciąża serwer a w folderze z plikami można dodać .htaccess z "deny from all". Problem tylko jest taki, że trzeba ten moduł zainstalować na serwerze czyli trzeba mieć dostęp do serwera (np. serwer dedykowany). Na serwerach wirtualnych raczej X-sendfile się nie znajdzie.

Tutaj jest ładnie opisane:

http://www.yiiframework.com/wiki/129/x-sendfile-serve-large-static-files-efficiently-from-web-applications/

Problem nr 2 rozwiązałem w ten sposób, że:

  • w kontrolerze dodałem akcję:



        public function actionDownload($id)

	{

	  $model=$this->loadModel($id);

	  

	  $file = Yii::app()->basePath . '/../images/' . $model->image;

	  

	  if (file_exists($file))

		{

			$fileDir=Yii::app()->basePath . '/../images/';

			Yii::app()->request->sendFile(

				$model->image,

				file_get_contents($fileDir . $model->image),

				$model->image

			);  

		}

		else

		{

			throw new CHttpException(404,'Żądany plik nie istnieje.');

			return $model;

		}

	}



  • uprawnienia do wykonania tej akcji w "public function accessRules()"

oraz w widoku częściowym dałem




<?php 

    echo CHtml::link("Pobierz plik", array('download', 'id'=>$data->id));

?>



Działa i chyba tak może zostać.

Pozostał jeszcze problem nr 1.

Mariusz W. pisze:

ale czy nie potrzeba beforeDelete(), żeby chociaż do zmiennej przypisać jaki plik ma usunąć?

Witaj,

Nie trzeba, ponieważ metoda afterDelete jest wykonywana na konkretnej instancji modelu, który reprezentuje konkretny rekord. Wygląda to mniej więcej tak:

  1. Tworzony jest model i do atrybutów pobierane są wartości z odpowiednich kolumn.

  2. Wywoływana jest akcja usuwania rekordu

  3. Po usunięciu rekordu w metodzie afterDelete masz dostęp do wartości atrybutów (np. $this->fileName)

Pozdrawiam Mariusz

Thx, za wyjaśnienie. Pomogło.

Dodałem w modelu i działa ładnie. Może komus sie przyda.




protected function afterDelete()

    {

		parent::afterDelete();

		

		$file = $this->logo;

		unlink(Yii::app()->basePath . '/../images/programs/' . $file);

    }



Witam,

z tym afterDelete, to nie jest taka pewna sprawa.

Problem polega na tym, że z bazy danych - jeżeli użytkownik w bazie ma odpowiednie uprawnienia i nie ma constraint’ów - rekord można zawsze usunąć bo nawet jeżeli odbywa się odczyt lub zapis to są to operacje atomowe. W przypadku usuwaniu pliku z dysku sprawa nie jest już taka pewna. Najprostszy przykład jest taki, że plik może być otwarty w innym skrypcie i usunięcie nie powiedzie się z powodu braku dostępu. Co wtedy? Rekord z bazy usunięty, a plik zostaje.

Lepszą opcją jest próba usunięcia pliku w beforeDelete. W przypadku niepowodzenia wyświetlasz błąd, w przypadku sukcesu usuwasz rekord z bazy danych. To czy użytkownik który łączy się z bazą ma uprawnienia i to czy gdzieś nie ma blokad zależy tylko i wyłącznie od projektu bazy danych i testów wykonanych przed usunięciem (chociażby w beforeDelete :) ).

Też zastanawiałem się nad tym, czy lepiej jest usuwać plik w beforeDelete czy afterDelete?

  1. Przypadek pesymistyczny w beforeDelete:

usuwany jest plik, natomiast jest problem podczas usuwania rekordu z bazy.

Wtedy gdzieś w systemie nadal pojawia się odniesienie do tego pliku (bo przecież informacja nadal jest w bazie danych), ale nie można nic zrobić, ponieważ plik nie istnieje.

  1. Przypadek pesymistyczny w afterDelete

usuwany jest rekord, natomiast wystąpił podczas usuwania pliku (np. tak jak pisałeś, inny skrypt działa na tym pliku). Nie ma wtedy problemu, że plik pokazuje się gdzieś tam w systemie. Natomiast mamy "porzucony" plik.

W tym przypadku można zapisać, że wystąpił taki problem do jakiegoś pliku, a pracownik monitorujący błędy, które występują w aplikacji wyłapie problem :slight_smile:

  1. Przypadek eliminujący problem

Zapisywać pliki w bazie :slight_smile: Łatwiesza synchronizacja. Nie mam w tym zakresie odpowiedniego doświadczenia i wiedzy, ale pewnie baza jest wtedy bardziej obciążona. Jednak na pewno istnieją rozwiązania, które eliminują i ten problem (w końcu całe bazy są przechowywane także w plikach).

Przypadek 1: można wyeliminować poprzez odpowiedni test przed usunięciem pliku (na przykład sprawdzając czy istnieją rekordy które mogłyby zablokować usunięcie).

Przypadek 2: należy upewnić się, że plik będzie usuwany tylko w jednym miejscu. Operacje rename, unlink mogą wywalić błędy jeżeli np. pierwszy skrypt otworzy plik, drugi skrypt wywoła rename, pierwszy skrypt zamknie plik i wywoła rename albo unlink. Nawet wywołanie file_exist() tuż przed unlink/rename nic nie da, gdyż dalej będą to 2 wywołania nie stanowiące operacji atomowej i prowadzące do sytuacji wyścigu.

Przypadek 3: ok dla mało obciążonych serwisów. Oprócz konieczności przechowywanie wszystkich meta-danych (czas utworzenia, uprawnienia, właściciel itp) jest sprawa wydajności. Pliki z dysku nieraz są (w zależności od serwera) przesyłane asynchronicznie z dysku bezpośrednio do interfejsu sieciowego za pomocą sendfile(). W przypadku db, dochodzi mnóstwo operacji dyskowych.

No i sprawa pieniędzy. Porównaj ceny 1 GB powierzchni dyskowej i ceny 1GB w bazie danych.

Przeczytaj to: http://perspectives…nsOfPhotos.aspx

O, niechcący wywołałem dyskusję.

Pkt. 3 - przechowywanie plików bezpośrednio w bazie wypróbowałem i jest OK w przypadku małych plików a w przypadku dużych obciąża bazę więc przyjąłem obecną metodę.

Pkt 1 lub 2 - chyba afterDelete jest bardziej bezpieczne niż beforeDelete, mimo iż może wystapić błąd że zostanie usunięty rekord z bazy a plik zostanie (w jednym momencie na rekordzie będzie wykonywana jedna operacja więc chyba nie grozi odwołanie do wykonania innej operacji w tym samym czasie).