STORAGE היא המחלקה שמקבלת קובץ (מ-$_FILES, מנתיב בדיסק, מכתובת URL או מ-base64), שומרת אותו במקום הנכון, רושמת אותו בטבלת CRM_storage ומחזירה מזהה מספרי. מכאן והלאה כל מקום באתר מחזיק את המזהה ומבקש ממנה כתובת או תמונה מוקטנת. בעמוד הזה: איך קובץ נשמר, מה האפשרויות של add(), איך קוראים את הקובץ בחזרה, ואיך מוחקים. הגנות ההעלאה מתוארות באבטחת העלאות, יצירת גדלים בכלי תמונה.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url. המחלקה נמצאת ב-system/collections/STORAGE.php; הפירוט המלא של כל המתודות נמצא גם ברפרנס STORAGE.
מודל הנתונים#
כל קובץ הוא שורה בטבלה CRM_storage (בבילדר: storage) וקובץ פיזי אחד, ועוד קבצי נגזרת (תמונות מוקטנות) לצידו.
| עמודה | משמעות |
|---|---|
id | המזהה שמחזירה add(). זה מה ששדות אחרים (תמונה ראשית, גלריה) שומרים |
uuid | מזהה אקראי, כשסוג השם הוא uuid |
name | השם המקורי של הקובץ, בלי סיומת. לתצוגה בלבד |
ext | הסיומת (אותיות קטנות) |
path | התיקייה (יחסית לשורש האתר), עם שם ה-bucket לפניה ב-S3 |
remote | מיקום האחסון: ""/מקומי, ftp או s3 |
data, copyrights, description | מטא-דאטה חופשית |
object_type, object_id, ord | קישור הקובץ לישות (למשל גלריה של כתבה) ומיון |
folder_id | התיקייה הווירטואלית במנהל המדיה |
filename_type | uuid או id: איך נקרא הקובץ הפיזי |
tumb | רשימה (בפורמט pipe) של הנגזרות שכבר נוצרו |
file_hash | md5 של התוכן, כשהעמודה קיימת (מזהה קבצים זהים) |
uploaded_by | מזהה המנהל שהעלה, כשההעלאה הייתה מהפאנל |
מה נשמר איפה#
הקובץ הפיזי נשמר בשם <uuid>.<ext> (ברירת המחדל) או <id>.<ext>, בתיקייה path. שם המקור של המשתמש לא משמש בנתיב: הוא נשמר רק בעמודה name. זה מונע התנגשויות וגם מעקר ניסיונות להשתמש בשם הקובץ כדי לכתוב למקום אחר.
media/Storage/3f2c9b1e-....jpg הקובץ
media/Storage/3f2c9b1e-...._tumb_400Xauto.jpg נגזרת
media/Storage/3f2c9b1e-...._wtm_20X-20.jpg סימן מים
סוג השם נקבע גלובלית ב-init.php:
STORAGE::set_filename_type("uuid"); // או "id"
תיקיית היעד ברירת מחדל: media/Storage. תיקיות חסרות נוצרות אוטומטית על ידי FILES::make_dir() במצב 0777, ולכן מכווצים אותן אחרי ההתקנה (התקשחות).
STORAGE::add()#
public static function add($file_path, $opts = array())
| פרמטר | משמעות |
|---|---|
$file_path | ערך מ-$_FILES (מערך עם tmp_name), נתיב בדיסק, או כתובת שמתחילה ב-http (נשלפת ב-cURL) |
$opts | מערך אפשרויות, בטבלה למטה |
ערך החזרה: עם save => true (ברירת המחדל): id של השורה החדשה. עם save => false: שם הקובץ שהוחלף. במקרה כשל: false. סיומת אסורה מסתיימת ב-die("STORAGE SECURITY ERROR!").
האפשרויות#
| אפשרות | ברירת מחדל | משמעות |
|---|---|---|
save | true | false = מחליף תוכן של קובץ קיים באותו שם ובאותה כתובת (ללא שורה חדשה) |
clear | false | מחק את הקובץ המקור אחרי ההעתקה (מופעל אוטומטית לקבצים שהורדו מ-URL) |
path | media/Storage | תיקיית היעד, יחסית לשורש האתר או נתיב מוחלט |
remote | CONFIG::$storage_remote | "", ftp, s3 |
name | שם המקור בלי סיומת | הערך לעמודה name |
object_type, object_id, ord | ריק | קישור לישות (נשמר רק כשהראשון והשני קיימים; object_id מומר ל-int) |
data, copyrights, description | ריק | מטא-דאטה |
folder_id | 0 | תיקיית מדיה |
filename_type | STORAGE::$filename_type | uuid או id לקובץ הזה |
img_convert | ריק | המרת פורמט: jpg, png, gif, webp |
img_max_width, img_max_height | ריק | הקטנה פרופורציונלית עד המידות (לעולם לא מגדילה) |
img_width, img_height | ריק | גודל מדויק, עם חיתוך |
דוגמאות#
העלאה מטופס, עם הקטנה והמרה ל-webp וקישור לכתבה:
// application/controllers/news_upload.php (מקור: טופס multipart עם שדה "photo")
if (!ADMIN::is_admin()) return "ERROR forbidden"; // אימות הוא אחריותכם
$id = STORAGE::add($_FILES["photo"], [
"object_type" => "news",
"object_id" => (int)$_GET["news_id"],
"img_convert" => "webp",
"img_max_width" => 1600,
"img_max_height" => 1600,
]);
if ($id === false) return "ERROR upload failed";
DB::update("news")->set_var("image", $id)->update((int)$_GET["news_id"]);
ייבוא מכתובת חיצונית או מקובץ בדיסק:
$id = STORAGE::add("https://example.org/pic.jpg", ["path" => "media/Imported"]);
$id = STORAGE::add(CONFIG::$base_path . "/import/logo.png");
כשמעבירים כתובת http..., STORAGE מורידה אותה בעצמה ב-cURL ועוקבת אחרי הפניות, ללא הגבלת דומיין. בליבה רק קוד של אדמינים קורא לזה. אל תעבירו לפונקציה כתובת שהמשתמש הקליד בלי אימות של הדומיין ושל הפרוטוקול: זו דלת לבקשות פנימיות (SSRF).
מ-base64 (למשל חתימה מ-canvas או צילום מהמצלמה):
public static function addFromBase64($base64, $name, $ext, $path, $remote = "local", $extra_settings = [])
$id = STORAGE::addFromBase64($_POST["signature"], "signature", "png", "media/Signatures");
אם הערך מתחיל ב-data:image/...;base64,, הסיומת נגזרת ממנו כשלא סופקה. הפונקציה מחזירה false כשהמידע ריק או פגום. אחרת מעבירה את הקובץ ל-add() עם $extra_settings.
יעדי אחסון#
remote | מה קורה | הגדרות ב-CONFIG_USER |
|---|---|---|
| ריק (מקומי) | copy() אל CONFIG::$base_path/<path>/<file> | אין |
ftp | ftp_put לשרת FTP, יוצר תיקיות לפי הצורך. אם ההעלאה נכשלת, השורה שנרשמה נמחקת | $storage_remote = "ftp", $storage_remote_url, $storage_remote_ftp_host, $storage_remote_ftp_username, $storage_remote_ftp_password |
s3 | putObject ל-bucket עם ACL public-read | $storage_remote = "s3" ומפתחות ב-MISC::$GLOBALS (Storage_remote_s3_key, Storage_remote_s3_secret, Storage_remote_s3_bucket) |
כתובת הקובץ נבנית לפי היעד: אתר מקומי CONFIG::$site_url . path/file, ב-FTP storage_remote_url, וב-S3 https://s3.amazonaws.com/<bucket>/....
בענף ה-S3 של add() אין break אחרי ההעלאה, כך שהקוד ממשיך אל ענף ההעתקה המקומי ויוצר גם עותק מקומי. אם אתם עובדים עם S3, בדקו את התיקייה המקומית ונקו עותקים מיותרים, ואל תניחו שהדיסק המקומי ריק.
קריאת קבצים#
STORAGE::get($id) // השורה, עם url, url_rel ו-filename מחושבים
STORAGE::get_url($id) // כתובת מלאה, או "" אם אין
STORAGE::get_thumb($id, 400, 300) // כתובת של תמונה מוקטנת (נוצרת בפעם הראשונה)
STORAGE::get_all("news", $newsId) // כל הקבצים של ישות, ממוינים לפי ord
STORAGE::fileExists($id) // האם הקובץ הפיזי קיים (ביעד המתאים)
get($id) שומרת תוצאה בזיכרון לאורך הבקשה. אם שיניתם שורה בעצמכם בתוך אותה בקשה, קראו ל-STORAGE::forget($id) כדי לקרוא מחדש.
| מתודה | חתימה | הערה |
|---|---|---|
get | get($id) | מקבל מזהה או שורה קיימת (מערך). מחזיר false אם אין |
get_all | get_all($object_type, $object_id, $ord = "ord ASC") | כל שורה עוברת get() |
get_url | get_url($id) | "" כשאין קובץ |
get_file_identifier | get_file_identifier($data) | uuid או id, לפי filename_type של השורה |
get_by_hash | get_by_hash($hash, $exclude_id = 0) | מציאת קובץ זהה לפי md5 |
get_local_file | get_local_file($id, $name = false) | נתיב מקומי; קובץ מרוחק מועתק לתיקיית ה-temp |
fileExists | fileExists($id) | בדיקה פיזית בדיסק, ב-FTP (בקשת HTTP) או ב-S3 |
בתבנית Smarty מעבירים מהקונטרולר את הכתובת המוכנה:
$this->view("news/item", [
"title" => $row["title"],
"image" => STORAGE::get_thumb($row["image"], 800, 450),
]);
<img src="{$image|escape}" alt="{$title|escape}" width="800" height="450">
מחיקה#
public static function remove($id, $only_files = false)
מוחקת את השורה (אלא אם $only_files הוא true), את הקובץ הפיזי ואת הנגזרות שלו. במקומי נמחקים הקובץ וכל <identifier>_tumb_*; ב-FTP נמחקים _tumb_ ו-_wtm_; ב-S3 נמחקים הקובץ ו-_tumb_. לא מחזירה ערך.
remove() לא יודעת מי עוד מחזיק את המזהה. אם שדה בכתבה עדיין מצביע על הקובץ, הוא יצביע על שורה שלא קיימת. מחקו או נקו את ההפניה לפני, או אחרי, באותה פעולה. וכמו כל מחיקה, היא צריכה הרשאה: אל תחשפו אותה ישירות לקלט משתמש.מה לא לעשות#
- אל תכתבו קבצי העלאה ישירות עם
move_uploaded_fileאוcopy()מהקוד שלכם: אתם מדלגים על חסימת הסיומות, על רישום הטבלה ועל ה-CDN. השתמשו ב-STORAGE::add(). - אל תבנו נתיב מקובץ מ-
$_GETאו$_POST(למשלSTORAGE::$temp_folder . "/" . $_POST["x"]) בליbasename()ובדיקה, כדי לא לפתוח מעבר לתיקייה. - אל תשרשרו נתונים שבאו מהמשתמש (למשל נתוני חיתוך) לשאילתת SQL גולמית סביב
STORAGE.save_thumb_db($sid, $ratio, $cropper_data)משרשרת את$cropper_dataל-SQL כמו שהוא, ולכן מעבירים לה רק ערך שאתם יצרתם.