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 @@
+
+
+
= t("User quotas") ?>
+
+
+
+
+
+
= t("Users") ?>
+
+
+
+
+ = t("Username") ?> |
+ = t("Full name") ?> |
+ = t("Fullsize") ?> |
+ = t("Resize") ?> |
+ = t("Thumbs") ?> |
+ = t("Total") ?> |
+ = t("Limit") ?> |
+
+
+ foreach ($users as $i => $user): ?>
+ $record = ORM::factory("users_space_usage")->where("owner_id", "=", $user->id)->find(); ?>
+ g-user = $user->admin ? "g-admin" : "" ?>">
+
+ "
+ alt="= html::clean_attribute($user->name) ?>"
+ width="20"
+ height="20" />
+ = html::clean($user->name) ?>
+ |
+
+ = html::clean($user->full_name) ?>
+ |
+
+ = number_format($record->partial_usage_mb("fullsize"), 2); ?> MB
+ |
+
+ = number_format($record->partial_usage_mb("resize"), 2); ?> MB
+ |
+
+ = number_format($record->partial_usage_mb("thumb"), 2); ?> MB
+ |
+
+ = number_format($record->total_usage_mb(), 2) ?> MB
+ |
+
+ = number_format($record->get_usage_limit_mb(), 2) ?> MB
+ |
+
+ endforeach ?>
+
+
+
+ = $theme->paginator() ?>
+
+
+
+
+
+
+
+
+
+
+
+
= t("Settings") ?>
+ = $quota_options ?>
+
+
+
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 @@
+
+
+
= t("User quotas") ?>
+
+
+
+
+
+
= t("Users") ?>
+
+
+
+
+ = t("Username") ?> |
+ = t("Full name") ?> |
+ = t("Fullsize") ?> |
+ = t("Resize") ?> |
+ = t("Thumbs") ?> |
+ = t("Total") ?> |
+ = t("Limit") ?> |
+
+
+ foreach ($users as $i => $user): ?>
+ $record = ORM::factory("users_space_usage")->where("owner_id", "=", $user->id)->find(); ?>
+ g-user = $user->admin ? "g-admin" : "" ?>">
+
+ "
+ alt="= html::clean_attribute($user->name) ?>"
+ width="20"
+ height="20" />
+ = html::clean($user->name) ?>
+ |
+
+ = html::clean($user->full_name) ?>
+ |
+
+ = number_format($record->partial_usage_mb("fullsize"), 2); ?> MB
+ |
+
+ = number_format($record->partial_usage_mb("resize"), 2); ?> MB
+ |
+
+ = number_format($record->partial_usage_mb("thumb"), 2); ?> MB
+ |
+
+ = number_format($record->total_usage_mb(), 2) ?> MB
+ |
+
+ = number_format($record->get_usage_limit_mb(), 2) ?> MB
+ |
+
+ endforeach ?>
+
+
+
+ = $theme->paginator() ?>
+
+
+
+
+
+
+
+
+
+
+
+
= t("Settings") ?>
+ = $quota_options ?>
+
+
+