From 21feda51e54bfb89f3a2bf205b64e603f4308f5f Mon Sep 17 00:00:00 2001 From: rWatcher Date: Mon, 12 Sep 2011 20:42:49 -0400 Subject: [PATCH] Initial commit of the Quotas module. --- .../quotas/controllers/admin_quotas.php | 152 ++++++++++++++++++ 3.0/modules/quotas/helpers/quotas_event.php | 110 +++++++++++++ .../quotas/helpers/quotas_installer.php | 47 ++++++ 3.0/modules/quotas/helpers/quotas_task.php | 126 +++++++++++++++ 3.0/modules/quotas/helpers/quotas_theme.php | 35 ++++ 3.0/modules/quotas/models/groups_quota.php | 21 +++ .../quotas/models/users_space_usage.php | 131 +++++++++++++++ 3.0/modules/quotas/module.info | 7 + .../quotas/views/admin_quotas.html.php | 98 +++++++++++ .../quotas/controllers/admin_quotas.php | 152 ++++++++++++++++++ 3.1/modules/quotas/helpers/quotas_event.php | 110 +++++++++++++ .../quotas/helpers/quotas_installer.php | 47 ++++++ 3.1/modules/quotas/helpers/quotas_task.php | 126 +++++++++++++++ 3.1/modules/quotas/helpers/quotas_theme.php | 35 ++++ 3.1/modules/quotas/models/groups_quota.php | 21 +++ .../quotas/models/users_space_usage.php | 131 +++++++++++++++ 3.1/modules/quotas/module.info | 7 + .../quotas/views/admin_quotas.html.php | 98 +++++++++++ 18 files changed, 1454 insertions(+) create mode 100644 3.0/modules/quotas/controllers/admin_quotas.php create mode 100644 3.0/modules/quotas/helpers/quotas_event.php create mode 100644 3.0/modules/quotas/helpers/quotas_installer.php create mode 100644 3.0/modules/quotas/helpers/quotas_task.php create mode 100644 3.0/modules/quotas/helpers/quotas_theme.php create mode 100644 3.0/modules/quotas/models/groups_quota.php create mode 100644 3.0/modules/quotas/models/users_space_usage.php create mode 100644 3.0/modules/quotas/module.info create mode 100644 3.0/modules/quotas/views/admin_quotas.html.php create mode 100644 3.1/modules/quotas/controllers/admin_quotas.php create mode 100644 3.1/modules/quotas/helpers/quotas_event.php create mode 100644 3.1/modules/quotas/helpers/quotas_installer.php create mode 100644 3.1/modules/quotas/helpers/quotas_task.php create mode 100644 3.1/modules/quotas/helpers/quotas_theme.php create mode 100644 3.1/modules/quotas/models/groups_quota.php create mode 100644 3.1/modules/quotas/models/users_space_usage.php create mode 100644 3.1/modules/quotas/module.info create mode 100644 3.1/modules/quotas/views/admin_quotas.html.php diff --git a/3.0/modules/quotas/controllers/admin_quotas.php b/3.0/modules/quotas/controllers/admin_quotas.php new file mode 100644 index 00000000..ac5bf8b5 --- /dev/null +++ b/3.0/modules/quotas/controllers/admin_quotas.php @@ -0,0 +1,152 @@ +page_title = t("Users and groups"); + $view->page_type = "collection"; + $view->page_subtype = "admin_users_quotas"; + $view->content = new View("admin_quotas.html"); + + $page_size = module::get_var("user", "page_size", 10); + $page = Input::instance()->get("page", "1"); + $builder = db::build(); + $user_count = $builder->from("users")->count_records(); + + $view->page = $page; + $view->page_size = $page_size; + $view->children_count = $user_count; + $view->max_pages = ceil($view->children_count / $view->page_size); + + $view->content->pager = new Pagination(); + $view->content->pager->initialize( + array("query_string" => "page", + "total_items" => $user_count, + "items_per_page" => $page_size, + "style" => "classic")); + + if ($page < 1) { + url::redirect(url::merge(array("page" => 1))); + } else if ($page > $view->content->pager->total_pages) { + url::redirect(url::merge(array("page" => $view->content->pager->total_pages))); + } + + $view->content->users = ORM::factory("user") + ->order_by("users.name", "ASC") + ->find_all($page_size, $view->content->pager->sql_offset); + $view->content->groups = ORM::factory("group")->order_by("name", "ASC")->find_all(); + $view->content->quota_options = $this->_get_quota_settings_form(); + print $view; + } + + public function form_group_quota($id) { + // Display the form for setting a quota for the specified group ($id). + $group = ORM::factory("group", $id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + print $this->_get_edit_group_quota($group); + } + + static function _get_edit_group_quota($group) { + // Generate a form for setting a quota for the specified group ($group). + $record = ORM::factory("groups_quota")->where("group_id", "=", $group->id)->find(); + $form = new Forge( + "admin/quotas/edit_quota/$group->id", "", "post", array("id" => "g-edit-quota-form")); + $group = $form->group("edit_quota")->label(t("Edit group quota")); + $group->input("group_quota")->label(t("Limit (MB)"))->id("g-group_quota")->value($record->storage_limit / 1024 / 1024) + ->error_messages("required", t("A value is required")); + + $group->submit("")->value(t("Save")); + return $form; + } + + public function edit_quota($id) { + // Save the specified quota to the database. + access::verify_csrf(); + + $group = ORM::factory("group", $id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + + $record = ORM::factory("groups_quota")->where("group_id", "=", $group->id)->find(); + $form = $this->_get_edit_group_quota($group); + try { + $valid = $form->validate(); + $record->group_id = $id; + $record->storage_limit = $form->edit_quota->inputs["group_quota"]->value * 1024 * 1024; + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_quota->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $record->save(); + message::success(t("Limit for group %group_name set", array("group_name" => $group->name))); + json::reply(array("result" => "success")); + } else { + json::reply(array("result" => "error", "html" => (string) $form)); + } + } + + private function _get_quota_settings_form() { + // Make a new form to allow the admin to specify how the system should calculate a user's quota. + $form = new Forge("admin/quotas/saveprefs", "", "post", + array("id" => "g-quotas-admin-form")); + + // Setup a checkbox for the form. + $quota_options["use_all_sizes"] = array(t("Count resizes and thumbnails towards a users limit?"), module::get_var("quotas", "use_all_sizes")); + $add_links = $form->group("quota_preferences"); + $add_links->checklist("quota_preferences_list") + ->options($quota_options); + + // Add a save button to the form. + $form->submit("save_preferences")->value(t("Save")); + + // Return the newly generated form. + return $form; + } + + public function saveprefs() { + // Prevent Cross Site Request Forgery + access::verify_csrf(); + + // Figure out which boxes where checked + $checkboxes_array = Input::instance()->post("quota_preferences_list"); + $use_all_sizes = false; + for ($i = 0; $i < count($checkboxes_array); $i++) { + if ($checkboxes_array[$i] == "use_all_sizes") { + $use_all_sizes = true; + } + } + + // Save Settings. + module::set_var("quotas", "use_all_sizes", $use_all_sizes); + message::success(t("Your Selection Has Been Saved.")); + + // Load Admin page. + url::redirect("admin/quotas"); + } +} diff --git a/3.0/modules/quotas/helpers/quotas_event.php b/3.0/modules/quotas/helpers/quotas_event.php new file mode 100644 index 00000000..aa832fd1 --- /dev/null +++ b/3.0/modules/quotas/helpers/quotas_event.php @@ -0,0 +1,110 @@ +get("content_menu") + ->append(Menu::factory("link") + ->id("quotas") + ->label(t("User quotas")) + ->url(url::site("admin/quotas"))); + } + + static function user_created($user) { + // Set up some default values whenever a new user is created. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $user->id)->find(); + if (!$record->loaded()) { + $record->owner_id = $user->id; + $record->fullsize = 0; + $record->resize = 0; + $record->thumb = 0; + $record->save(); + } + } + + static function user_before_delete($user) { + // When deleting a user, all of that user's items get re-assigned to the admin account, + // so the file sizes need to be reassigned to the admin user as well. + $admin = identity::admin_user(); + $admin_record = ORM::factory("users_space_usage")->where("owner_id", "=", $admin->id)->find(); + $deleted_user_record = ORM::factory("users_space_usage")->where("owner_id", "=", $user->id)->find(); + if ($deleted_user_record->loaded()) { + $admin_record->fullsize = $admin_record->fullsize + $deleted_user_record->fullsize; + $admin_record->resize = $admin_record->resize + $deleted_user_record->resize; + $admin_record->thumb = $admin_record->thumb + $deleted_user_record->thumb; + $admin_record->save(); + $deleted_user_record->delete(); + } + } + + static function item_before_create($item) { + // When creating a new item, make sure it's file size won't put the user over their limit. + // If it does, throw an error, which will prevent gallery from accepting the file. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + if (!$record->loaded()) { + $record->owner_id = $item->owner_id; + } + if ($record->get_usage_limit() == 0) { + return; + } + if ((filesize($item->data_file) + $record->current_usage()) > $record->get_usage_limit()) { + throw new Exception($item->name . " rejected, user #" . $item->owner_id . " over limit."); + } + } + + static function item_created($item) { + // When a new item is created, add it's file size to the users_space_usage table. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + if (!$record->loaded()) { + $record->owner_id = $item->owner_id; + $record->fullsize = 0; + $record->resize = 0; + $record->thumb = 0; + $record->save(); + } + $record->add_item($item); + } + + static function item_before_delete($item) { + // When an item is deleted, remove it's file size from the users_space_usage table. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + $record->remove_item($item); + } + + // I can't monitor the item_before_update / item_updated events to adjust for rotated photos, + // because they fire when a new photo is uploaded (before it's created) and cause all kinds of weirdness. + // So instead, I'm using graphics_rotate to detect a rotate and remove the existing file sizes, and + // item_updated_data_file to add in the new data file sizes. + // Does item_updated_data_file fire for any other reason? (watermarking? renaming/moving/deleting/keeporiginal do not cause updated_data_file.) + static function graphics_rotate($input_file, $output_file, $options) { + // Remove the current item's file size from the quotas table. + $item = item::find_by_path(substr(str_replace(VARPATH, "", $input_file), strpos(str_replace(VARPATH, "", $input_file), "/")+1)); + if ($item->loaded()) { + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + $record->remove_item($item); + } + } + + static function item_updated_data_file($item) { + // Add the current item's file size into the quotas table. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + $record->add_item($item); + } +} diff --git a/3.0/modules/quotas/helpers/quotas_installer.php b/3.0/modules/quotas/helpers/quotas_installer.php new file mode 100644 index 00000000..6eb46f4a --- /dev/null +++ b/3.0/modules/quotas/helpers/quotas_installer.php @@ -0,0 +1,47 @@ +query("CREATE TABLE IF NOT EXISTS {users_space_usages} ( + `id` int(9) NOT NULL auto_increment, + `owner_id` int(9) NOT NULL, + `fullsize` BIGINT UNSIGNED NOT NULL, + `resize` BIGINT UNSIGNED NOT NULL, + `thumb` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + KEY(`owner_id`, `id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE IF NOT EXISTS {groups_quotas} ( + `id` int(9) NOT NULL auto_increment, + `group_id` int(9) NOT NULL, + `storage_limit` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + KEY(`group_id`, `id`)) + DEFAULT CHARSET=utf8;"); + + module::set_var("quotas", "use_all_sizes", true); + + // Set the module version number. + module::set_version("quotas", 1); + } +} diff --git a/3.0/modules/quotas/helpers/quotas_task.php b/3.0/modules/quotas/helpers/quotas_task.php new file mode 100644 index 00000000..4413520b --- /dev/null +++ b/3.0/modules/quotas/helpers/quotas_task.php @@ -0,0 +1,126 @@ +where("users.guest", "=", "0") + ->join("users_space_usages", "users.id", "users_space_usages.owner_id", "LEFT OUTER") + ->and_where("users_space_usages.owner_id", "IS", NULL)->count_all(); + + $tasks = array(); + $tasks[] = Task_Definition::factory() + ->callback("quotas_task::update_quotasdb") + ->name(t("Rebuild user quotas table")) + ->description(t("Recalculates each users space usage.")) + ->severity($missing_users ? log::WARNING : log::SUCCESS); + + return $tasks; + } + + static function update_quotasdb($task) { + // Re-create the users_space_usages table and recalculate all values. + + // Retrieve the total variable. If this is the first time this function has been run, + // total will be empty. + $total = $task->get("total"); + $existing_items = ORM::factory("item")->where("type", "!=", "album")->find_all(); + + if (empty($total)) { + // If this is the first time this function has been run, + // delete and re-create the users_space_usages table, and set up + // some initial variables. + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {users_space_usages};"); + $db->query("CREATE TABLE IF NOT EXISTS {users_space_usages} ( + `id` int(9) NOT NULL auto_increment, + `owner_id` int(9) NOT NULL, + `fullsize` BIGINT UNSIGNED NOT NULL, + `resize` BIGINT UNSIGNED NOT NULL, + `thumb` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + KEY(`owner_id`, `id`)) + DEFAULT CHARSET=utf8;"); + + // Set the initial values for all variables. + $task->set("total", count($existing_items)); + $total = $task->get("total"); + $task->set("last_id", 0); + $task->set("completed_items", 0); + $task->set("total_users", ORM::factory("user")->where("guest", "=", "0")->count_all()); + $task->set("completed_users", 0); + $task->set("last_user_id", 0); + } + + // Retrieve the values for variables from the last time this + // function was run. + $last_id = $task->get("last_id"); + $completed_items = $task->get("completed_items"); + $total_users = $task->get("total_users"); + $completed_users = $task->get("completed_users"); + $last_user_id = $task->get("last_user_id"); + + // First set up default values for all non-guest users. + if ($total_users > $completed_users) { + $one_user = ORM::factory("user") + ->where("guest", "=", "0") + ->where("id", ">", $last_user_id) + ->order_by("id") + ->find_all(1); + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $one_user[0]->id)->find(); + if (!$record->loaded()) { + $record->owner_id = $one_user[0]->id; + $record->fullsize = 0; + $record->resize = 0; + $record->thumb = 0; + $record->save(); + } + $task->set("last_user_id", $one_user[0]->id); + $task->set("completed_users", ++$completed_users); + $task->status = t("Populating quotas table..."); + + } else { + // Loop through each non-album item in Gallery and log its file size to its owner. + $item = ORM::factory("item") + ->where("type", "!=", "album") + ->where("id", ">", $last_id) + ->order_by("id") + ->find_all(1); + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item[0]->owner_id)->find(); + $record->add_item($item[0]); + + // Store the current position and update the status message. + $task->set("last_id", $item[0]->id); + $task->set("completed_items", ++$completed_items); + if ($total == $completed_items) { + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + $task->status = t("Complete"); + } else { + $task->percent_complete = round(100 * $completed_items / $total); + $task->status = t("Scanning $completed_items of $total files"); + } + } + } +} diff --git a/3.0/modules/quotas/helpers/quotas_theme.php b/3.0/modules/quotas/helpers/quotas_theme.php new file mode 100644 index 00000000..7a4e0916 --- /dev/null +++ b/3.0/modules/quotas/helpers/quotas_theme.php @@ -0,0 +1,35 @@ +guest) { + $record = ORM::factory("users_space_usage")->where("owner_id", "=", identity::active_user()->id)->find(); + if ($record->get_usage_limit() == 0) { + print t("You are using %usage MB", array("usage" => number_format($record->total_usage_mb(), 2))); + } else { + print t("You are using %usage of your %limit MB limit (%percentage%)", + array("usage" => number_format($record->current_usage_mb(), 2), + "limit" => number_format($record->get_usage_limit_mb(), 2), + "percentage" => number_format((($record->current_usage() / $record->get_usage_limit()) * 100), 2))); + } + } + } +} diff --git a/3.0/modules/quotas/models/groups_quota.php b/3.0/modules/quotas/models/groups_quota.php new file mode 100644 index 00000000..25af34e1 --- /dev/null +++ b/3.0/modules/quotas/models/groups_quota.php @@ -0,0 +1,21 @@ +$file_type / 1024 / 1024); + } + + public function total_usage() { + // Return the user's total usage in bytes. + return ($this->fullsize + $this->resize + $this->thumb); + } + + public function total_usage_mb() { + // Return the user's total usage in megabytes. + return (($this->total_usage()) / 1024 / 1024); + } + + public function current_usage() { + // Return the users relevant usage in bytes based on the use_all_sizes setting. + if (module::get_var("quotas", "use_all_sizes") == true) { + return $this->total_usage(); + } else { + return $this->fullsize; + } + } + + public function current_usage_mb() { + // Return the users relevant usage in megabytes based on the use_all_sizes setting. + return ($this->current_usage() / 1024 / 1024); + } + + public function get_usage_limit() { + // Returns a user's maximum limit in bytes. + $user_groups = ORM::factory("group") + ->join("groups_users", "groups_users.group_id", "groups.id") + ->join("groups_quotas", "groups_quotas.group_id", "groups.id") + ->select("groups.id") + ->select("groups_quotas.storage_limit") + ->where("groups_users.user_id", "=", $this->owner_id) + ->order_by("groups_quotas.storage_limit", "DESC") + ->find_all(1); + if (!empty($user_groups)) { + if ($user_groups[0]->storage_limit <= "0") { + return 0; + } else { + return $user_groups[0]->storage_limit; + } + } + return 0; + } + + public function get_usage_limit_mb() { + // Returns a user's maximum limit in megabytes. + return ($this->get_usage_limit() / 1024 / 1024); + } + + public function add_item($item) { + // Adds an item's file size to the table. + if ($item->is_album()) { + return ; + } + + $item_fullsize = 0; + $item_resize = 0; + $item_thumb = 0; + + if (file_exists($item->file_path())) { + $item_fullsize = filesize($item->file_path()); + } + if (file_exists($item->thumb_path())) { + $item_thumb = filesize($item->thumb_path()); + } + if (file_exists($item->resize_path())) { + $item_resize = filesize($item->resize_path()); + } + + $this->fullsize = $this->fullsize + $item_fullsize; + $this->resize = $this->resize + $item_resize; + $this->thumb = $this->thumb + $item_thumb; + $this->save(); + + return ; + } + + public function remove_item($item) { + // Removes an item's file size from the table. + if ($item->is_album()) { + return ; + } + + $item_fullsize = 0; + $item_resize = 0; + $item_thumb = 0; + + if (file_exists($item->file_path())) { + $item_fullsize = filesize($item->file_path()); + } + if (file_exists($item->thumb_path())) { + $item_thumb = filesize($item->thumb_path()); + } + if (file_exists($item->resize_path())) { + $item_resize = filesize($item->resize_path()); + } + + $this->fullsize = $this->fullsize - $item_fullsize; + $this->resize = $this->resize - $item_resize; + $this->thumb = $this->thumb - $item_thumb; + $this->save(); + + return ; + } +} diff --git a/3.0/modules/quotas/module.info b/3.0/modules/quotas/module.info new file mode 100644 index 00000000..11f9f668 --- /dev/null +++ b/3.0/modules/quotas/module.info @@ -0,0 +1,7 @@ +name = "Quotas" +description = "Assign quotas to user groups and track each users space usage." +version = 1 +author_name = "rWatcher" +author_url = "http://codex.gallery2.org/User:RWatcher" +info_url = "http://codex.gallery2.org/Gallery3:Modules:quotas" +discuss_url = "http://gallery.menalto.com/node/103606" diff --git a/3.0/modules/quotas/views/admin_quotas.html.php b/3.0/modules/quotas/views/admin_quotas.html.php new file mode 100644 index 00000000..e1f63763 --- /dev/null +++ b/3.0/modules/quotas/views/admin_quotas.html.php @@ -0,0 +1,98 @@ + +
+

+ +
+ +
+ +

+ +
+ + + + + + + + + + + + $user): ?> + where("owner_id", "=", $user->id)->find(); ?> + g-user admin ? "g-admin" : "" ?>"> + + + + + + + + + +
+ " + alt="name) ?>" + width="20" + height="20" /> + name) ?> + + full_name) ?> + + partial_usage_mb("fullsize"), 2); ?> MB + + partial_usage_mb("resize"), 2); ?> MB + + partial_usage_mb("thumb"), 2); ?> MB + + total_usage_mb(), 2) ?> MB + + get_usage_limit_mb(), 2) ?> MB +
+ +
+ paginator() ?> +
+ +
+
+ +
+

+ + + + + + + $group): ?> + where("group_id", "=", $group->id)->find(); ?> + g-user "> + + + + + +
+ name) ?> + + storage_limit / 1024 / 1024, 2); ?> MB + + id") ?>" + open_text="" + class="g-panel-link g-button ui-state-default ui-corner-all ui-icon-left"> + +
+
+ +
+
+ +
+

+ +
+
+
diff --git a/3.1/modules/quotas/controllers/admin_quotas.php b/3.1/modules/quotas/controllers/admin_quotas.php new file mode 100644 index 00000000..ac5bf8b5 --- /dev/null +++ b/3.1/modules/quotas/controllers/admin_quotas.php @@ -0,0 +1,152 @@ +page_title = t("Users and groups"); + $view->page_type = "collection"; + $view->page_subtype = "admin_users_quotas"; + $view->content = new View("admin_quotas.html"); + + $page_size = module::get_var("user", "page_size", 10); + $page = Input::instance()->get("page", "1"); + $builder = db::build(); + $user_count = $builder->from("users")->count_records(); + + $view->page = $page; + $view->page_size = $page_size; + $view->children_count = $user_count; + $view->max_pages = ceil($view->children_count / $view->page_size); + + $view->content->pager = new Pagination(); + $view->content->pager->initialize( + array("query_string" => "page", + "total_items" => $user_count, + "items_per_page" => $page_size, + "style" => "classic")); + + if ($page < 1) { + url::redirect(url::merge(array("page" => 1))); + } else if ($page > $view->content->pager->total_pages) { + url::redirect(url::merge(array("page" => $view->content->pager->total_pages))); + } + + $view->content->users = ORM::factory("user") + ->order_by("users.name", "ASC") + ->find_all($page_size, $view->content->pager->sql_offset); + $view->content->groups = ORM::factory("group")->order_by("name", "ASC")->find_all(); + $view->content->quota_options = $this->_get_quota_settings_form(); + print $view; + } + + public function form_group_quota($id) { + // Display the form for setting a quota for the specified group ($id). + $group = ORM::factory("group", $id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + print $this->_get_edit_group_quota($group); + } + + static function _get_edit_group_quota($group) { + // Generate a form for setting a quota for the specified group ($group). + $record = ORM::factory("groups_quota")->where("group_id", "=", $group->id)->find(); + $form = new Forge( + "admin/quotas/edit_quota/$group->id", "", "post", array("id" => "g-edit-quota-form")); + $group = $form->group("edit_quota")->label(t("Edit group quota")); + $group->input("group_quota")->label(t("Limit (MB)"))->id("g-group_quota")->value($record->storage_limit / 1024 / 1024) + ->error_messages("required", t("A value is required")); + + $group->submit("")->value(t("Save")); + return $form; + } + + public function edit_quota($id) { + // Save the specified quota to the database. + access::verify_csrf(); + + $group = ORM::factory("group", $id); + if (empty($group)) { + throw new Kohana_404_Exception(); + } + + $record = ORM::factory("groups_quota")->where("group_id", "=", $group->id)->find(); + $form = $this->_get_edit_group_quota($group); + try { + $valid = $form->validate(); + $record->group_id = $id; + $record->storage_limit = $form->edit_quota->inputs["group_quota"]->value * 1024 * 1024; + } catch (ORM_Validation_Exception $e) { + // Translate ORM validation errors into form error messages + foreach ($e->validation->errors() as $key => $error) { + $form->edit_quota->inputs[$key]->add_error($error, 1); + } + $valid = false; + } + + if ($valid) { + $record->save(); + message::success(t("Limit for group %group_name set", array("group_name" => $group->name))); + json::reply(array("result" => "success")); + } else { + json::reply(array("result" => "error", "html" => (string) $form)); + } + } + + private function _get_quota_settings_form() { + // Make a new form to allow the admin to specify how the system should calculate a user's quota. + $form = new Forge("admin/quotas/saveprefs", "", "post", + array("id" => "g-quotas-admin-form")); + + // Setup a checkbox for the form. + $quota_options["use_all_sizes"] = array(t("Count resizes and thumbnails towards a users limit?"), module::get_var("quotas", "use_all_sizes")); + $add_links = $form->group("quota_preferences"); + $add_links->checklist("quota_preferences_list") + ->options($quota_options); + + // Add a save button to the form. + $form->submit("save_preferences")->value(t("Save")); + + // Return the newly generated form. + return $form; + } + + public function saveprefs() { + // Prevent Cross Site Request Forgery + access::verify_csrf(); + + // Figure out which boxes where checked + $checkboxes_array = Input::instance()->post("quota_preferences_list"); + $use_all_sizes = false; + for ($i = 0; $i < count($checkboxes_array); $i++) { + if ($checkboxes_array[$i] == "use_all_sizes") { + $use_all_sizes = true; + } + } + + // Save Settings. + module::set_var("quotas", "use_all_sizes", $use_all_sizes); + message::success(t("Your Selection Has Been Saved.")); + + // Load Admin page. + url::redirect("admin/quotas"); + } +} diff --git a/3.1/modules/quotas/helpers/quotas_event.php b/3.1/modules/quotas/helpers/quotas_event.php new file mode 100644 index 00000000..aa832fd1 --- /dev/null +++ b/3.1/modules/quotas/helpers/quotas_event.php @@ -0,0 +1,110 @@ +get("content_menu") + ->append(Menu::factory("link") + ->id("quotas") + ->label(t("User quotas")) + ->url(url::site("admin/quotas"))); + } + + static function user_created($user) { + // Set up some default values whenever a new user is created. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $user->id)->find(); + if (!$record->loaded()) { + $record->owner_id = $user->id; + $record->fullsize = 0; + $record->resize = 0; + $record->thumb = 0; + $record->save(); + } + } + + static function user_before_delete($user) { + // When deleting a user, all of that user's items get re-assigned to the admin account, + // so the file sizes need to be reassigned to the admin user as well. + $admin = identity::admin_user(); + $admin_record = ORM::factory("users_space_usage")->where("owner_id", "=", $admin->id)->find(); + $deleted_user_record = ORM::factory("users_space_usage")->where("owner_id", "=", $user->id)->find(); + if ($deleted_user_record->loaded()) { + $admin_record->fullsize = $admin_record->fullsize + $deleted_user_record->fullsize; + $admin_record->resize = $admin_record->resize + $deleted_user_record->resize; + $admin_record->thumb = $admin_record->thumb + $deleted_user_record->thumb; + $admin_record->save(); + $deleted_user_record->delete(); + } + } + + static function item_before_create($item) { + // When creating a new item, make sure it's file size won't put the user over their limit. + // If it does, throw an error, which will prevent gallery from accepting the file. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + if (!$record->loaded()) { + $record->owner_id = $item->owner_id; + } + if ($record->get_usage_limit() == 0) { + return; + } + if ((filesize($item->data_file) + $record->current_usage()) > $record->get_usage_limit()) { + throw new Exception($item->name . " rejected, user #" . $item->owner_id . " over limit."); + } + } + + static function item_created($item) { + // When a new item is created, add it's file size to the users_space_usage table. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + if (!$record->loaded()) { + $record->owner_id = $item->owner_id; + $record->fullsize = 0; + $record->resize = 0; + $record->thumb = 0; + $record->save(); + } + $record->add_item($item); + } + + static function item_before_delete($item) { + // When an item is deleted, remove it's file size from the users_space_usage table. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + $record->remove_item($item); + } + + // I can't monitor the item_before_update / item_updated events to adjust for rotated photos, + // because they fire when a new photo is uploaded (before it's created) and cause all kinds of weirdness. + // So instead, I'm using graphics_rotate to detect a rotate and remove the existing file sizes, and + // item_updated_data_file to add in the new data file sizes. + // Does item_updated_data_file fire for any other reason? (watermarking? renaming/moving/deleting/keeporiginal do not cause updated_data_file.) + static function graphics_rotate($input_file, $output_file, $options) { + // Remove the current item's file size from the quotas table. + $item = item::find_by_path(substr(str_replace(VARPATH, "", $input_file), strpos(str_replace(VARPATH, "", $input_file), "/")+1)); + if ($item->loaded()) { + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + $record->remove_item($item); + } + } + + static function item_updated_data_file($item) { + // Add the current item's file size into the quotas table. + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item->owner_id)->find(); + $record->add_item($item); + } +} diff --git a/3.1/modules/quotas/helpers/quotas_installer.php b/3.1/modules/quotas/helpers/quotas_installer.php new file mode 100644 index 00000000..6eb46f4a --- /dev/null +++ b/3.1/modules/quotas/helpers/quotas_installer.php @@ -0,0 +1,47 @@ +query("CREATE TABLE IF NOT EXISTS {users_space_usages} ( + `id` int(9) NOT NULL auto_increment, + `owner_id` int(9) NOT NULL, + `fullsize` BIGINT UNSIGNED NOT NULL, + `resize` BIGINT UNSIGNED NOT NULL, + `thumb` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + KEY(`owner_id`, `id`)) + DEFAULT CHARSET=utf8;"); + + $db->query("CREATE TABLE IF NOT EXISTS {groups_quotas} ( + `id` int(9) NOT NULL auto_increment, + `group_id` int(9) NOT NULL, + `storage_limit` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + KEY(`group_id`, `id`)) + DEFAULT CHARSET=utf8;"); + + module::set_var("quotas", "use_all_sizes", true); + + // Set the module version number. + module::set_version("quotas", 1); + } +} diff --git a/3.1/modules/quotas/helpers/quotas_task.php b/3.1/modules/quotas/helpers/quotas_task.php new file mode 100644 index 00000000..4413520b --- /dev/null +++ b/3.1/modules/quotas/helpers/quotas_task.php @@ -0,0 +1,126 @@ +where("users.guest", "=", "0") + ->join("users_space_usages", "users.id", "users_space_usages.owner_id", "LEFT OUTER") + ->and_where("users_space_usages.owner_id", "IS", NULL)->count_all(); + + $tasks = array(); + $tasks[] = Task_Definition::factory() + ->callback("quotas_task::update_quotasdb") + ->name(t("Rebuild user quotas table")) + ->description(t("Recalculates each users space usage.")) + ->severity($missing_users ? log::WARNING : log::SUCCESS); + + return $tasks; + } + + static function update_quotasdb($task) { + // Re-create the users_space_usages table and recalculate all values. + + // Retrieve the total variable. If this is the first time this function has been run, + // total will be empty. + $total = $task->get("total"); + $existing_items = ORM::factory("item")->where("type", "!=", "album")->find_all(); + + if (empty($total)) { + // If this is the first time this function has been run, + // delete and re-create the users_space_usages table, and set up + // some initial variables. + $db = Database::instance(); + $db->query("DROP TABLE IF EXISTS {users_space_usages};"); + $db->query("CREATE TABLE IF NOT EXISTS {users_space_usages} ( + `id` int(9) NOT NULL auto_increment, + `owner_id` int(9) NOT NULL, + `fullsize` BIGINT UNSIGNED NOT NULL, + `resize` BIGINT UNSIGNED NOT NULL, + `thumb` BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + KEY(`owner_id`, `id`)) + DEFAULT CHARSET=utf8;"); + + // Set the initial values for all variables. + $task->set("total", count($existing_items)); + $total = $task->get("total"); + $task->set("last_id", 0); + $task->set("completed_items", 0); + $task->set("total_users", ORM::factory("user")->where("guest", "=", "0")->count_all()); + $task->set("completed_users", 0); + $task->set("last_user_id", 0); + } + + // Retrieve the values for variables from the last time this + // function was run. + $last_id = $task->get("last_id"); + $completed_items = $task->get("completed_items"); + $total_users = $task->get("total_users"); + $completed_users = $task->get("completed_users"); + $last_user_id = $task->get("last_user_id"); + + // First set up default values for all non-guest users. + if ($total_users > $completed_users) { + $one_user = ORM::factory("user") + ->where("guest", "=", "0") + ->where("id", ">", $last_user_id) + ->order_by("id") + ->find_all(1); + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $one_user[0]->id)->find(); + if (!$record->loaded()) { + $record->owner_id = $one_user[0]->id; + $record->fullsize = 0; + $record->resize = 0; + $record->thumb = 0; + $record->save(); + } + $task->set("last_user_id", $one_user[0]->id); + $task->set("completed_users", ++$completed_users); + $task->status = t("Populating quotas table..."); + + } else { + // Loop through each non-album item in Gallery and log its file size to its owner. + $item = ORM::factory("item") + ->where("type", "!=", "album") + ->where("id", ">", $last_id) + ->order_by("id") + ->find_all(1); + $record = ORM::factory("users_space_usage")->where("owner_id", "=", $item[0]->owner_id)->find(); + $record->add_item($item[0]); + + // Store the current position and update the status message. + $task->set("last_id", $item[0]->id); + $task->set("completed_items", ++$completed_items); + if ($total == $completed_items) { + $task->done = true; + $task->state = "success"; + $task->percent_complete = 100; + $task->status = t("Complete"); + } else { + $task->percent_complete = round(100 * $completed_items / $total); + $task->status = t("Scanning $completed_items of $total files"); + } + } + } +} diff --git a/3.1/modules/quotas/helpers/quotas_theme.php b/3.1/modules/quotas/helpers/quotas_theme.php new file mode 100644 index 00000000..7a4e0916 --- /dev/null +++ b/3.1/modules/quotas/helpers/quotas_theme.php @@ -0,0 +1,35 @@ +guest) { + $record = ORM::factory("users_space_usage")->where("owner_id", "=", identity::active_user()->id)->find(); + if ($record->get_usage_limit() == 0) { + print t("You are using %usage MB", array("usage" => number_format($record->total_usage_mb(), 2))); + } else { + print t("You are using %usage of your %limit MB limit (%percentage%)", + array("usage" => number_format($record->current_usage_mb(), 2), + "limit" => number_format($record->get_usage_limit_mb(), 2), + "percentage" => number_format((($record->current_usage() / $record->get_usage_limit()) * 100), 2))); + } + } + } +} diff --git a/3.1/modules/quotas/models/groups_quota.php b/3.1/modules/quotas/models/groups_quota.php new file mode 100644 index 00000000..25af34e1 --- /dev/null +++ b/3.1/modules/quotas/models/groups_quota.php @@ -0,0 +1,21 @@ +$file_type / 1024 / 1024); + } + + public function total_usage() { + // Return the user's total usage in bytes. + return ($this->fullsize + $this->resize + $this->thumb); + } + + public function total_usage_mb() { + // Return the user's total usage in megabytes. + return (($this->total_usage()) / 1024 / 1024); + } + + public function current_usage() { + // Return the users relevant usage in bytes based on the use_all_sizes setting. + if (module::get_var("quotas", "use_all_sizes") == true) { + return $this->total_usage(); + } else { + return $this->fullsize; + } + } + + public function current_usage_mb() { + // Return the users relevant usage in megabytes based on the use_all_sizes setting. + return ($this->current_usage() / 1024 / 1024); + } + + public function get_usage_limit() { + // Returns a user's maximum limit in bytes. + $user_groups = ORM::factory("group") + ->join("groups_users", "groups_users.group_id", "groups.id") + ->join("groups_quotas", "groups_quotas.group_id", "groups.id") + ->select("groups.id") + ->select("groups_quotas.storage_limit") + ->where("groups_users.user_id", "=", $this->owner_id) + ->order_by("groups_quotas.storage_limit", "DESC") + ->find_all(1); + if (!empty($user_groups)) { + if ($user_groups[0]->storage_limit <= "0") { + return 0; + } else { + return $user_groups[0]->storage_limit; + } + } + return 0; + } + + public function get_usage_limit_mb() { + // Returns a user's maximum limit in megabytes. + return ($this->get_usage_limit() / 1024 / 1024); + } + + public function add_item($item) { + // Adds an item's file size to the table. + if ($item->is_album()) { + return ; + } + + $item_fullsize = 0; + $item_resize = 0; + $item_thumb = 0; + + if (file_exists($item->file_path())) { + $item_fullsize = filesize($item->file_path()); + } + if (file_exists($item->thumb_path())) { + $item_thumb = filesize($item->thumb_path()); + } + if (file_exists($item->resize_path())) { + $item_resize = filesize($item->resize_path()); + } + + $this->fullsize = $this->fullsize + $item_fullsize; + $this->resize = $this->resize + $item_resize; + $this->thumb = $this->thumb + $item_thumb; + $this->save(); + + return ; + } + + public function remove_item($item) { + // Removes an item's file size from the table. + if ($item->is_album()) { + return ; + } + + $item_fullsize = 0; + $item_resize = 0; + $item_thumb = 0; + + if (file_exists($item->file_path())) { + $item_fullsize = filesize($item->file_path()); + } + if (file_exists($item->thumb_path())) { + $item_thumb = filesize($item->thumb_path()); + } + if (file_exists($item->resize_path())) { + $item_resize = filesize($item->resize_path()); + } + + $this->fullsize = $this->fullsize - $item_fullsize; + $this->resize = $this->resize - $item_resize; + $this->thumb = $this->thumb - $item_thumb; + $this->save(); + + return ; + } +} diff --git a/3.1/modules/quotas/module.info b/3.1/modules/quotas/module.info new file mode 100644 index 00000000..11f9f668 --- /dev/null +++ b/3.1/modules/quotas/module.info @@ -0,0 +1,7 @@ +name = "Quotas" +description = "Assign quotas to user groups and track each users space usage." +version = 1 +author_name = "rWatcher" +author_url = "http://codex.gallery2.org/User:RWatcher" +info_url = "http://codex.gallery2.org/Gallery3:Modules:quotas" +discuss_url = "http://gallery.menalto.com/node/103606" diff --git a/3.1/modules/quotas/views/admin_quotas.html.php b/3.1/modules/quotas/views/admin_quotas.html.php new file mode 100644 index 00000000..e1f63763 --- /dev/null +++ b/3.1/modules/quotas/views/admin_quotas.html.php @@ -0,0 +1,98 @@ + +
+

+ +
+ +
+ +

+ +
+ + + + + + + + + + + + $user): ?> + where("owner_id", "=", $user->id)->find(); ?> + g-user admin ? "g-admin" : "" ?>"> + + + + + + + + + +
+ " + alt="name) ?>" + width="20" + height="20" /> + name) ?> + + full_name) ?> + + partial_usage_mb("fullsize"), 2); ?> MB + + partial_usage_mb("resize"), 2); ?> MB + + partial_usage_mb("thumb"), 2); ?> MB + + total_usage_mb(), 2) ?> MB + + get_usage_limit_mb(), 2) ?> MB +
+ +
+ paginator() ?> +
+ +
+
+ +
+

+ + + + + + + $group): ?> + where("group_id", "=", $group->id)->find(); ?> + g-user "> + + + + + +
+ name) ?> + + storage_limit / 1024 / 1024, 2); ?> MB + + id") ?>" + open_text="" + class="g-panel-link g-button ui-state-default ui-corner-all ui-icon-left"> + +
+
+ +
+
+ +
+

+ +
+
+