Mam 2 pytania bo nijak nie mogę sobie poradzić, przejrzałem dużo postów ale nie znalazłem odpowiedniego rozwiązania.
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
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.
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.
To jest chyba proste
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:
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.
Nie trzeba, ponieważ metoda afterDelete jest wykonywana na konkretnej instancji modelu, który reprezentuje konkretny rekord. Wygląda to mniej więcej tak:
Tworzony jest model i do atrybutów pobierane są wartości z odpowiednich kolumn.
Wywoływana jest akcja usuwania rekordu
Po usunięciu rekordu w metodzie afterDelete masz dostęp do wartości atrybutów (np. $this->fileName)
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?
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.
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
Przypadek eliminujący problem
Zapisywać pliki w bazie Ł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.
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).