If you use disk quota then you give yourself an awful lot of work. There is, in fact, an approximate solution in PostgreSQL, with some minor tinkering and no need to make a large number of tablespaces (schemas will still be a good idea to give every user his/her own namespace).
The function pg_total_relation_size(regclass)
gives you the total disk space used for a table, including its indexes and TOAST tables. So scan pg_class
and sum up:
CREATE VIEW user_disk_usage AS
SELECT r.rolname, SUM(pg_total_relation_size(c.oid)) AS total_disk_usage
FROM pg_class c, pg_roles r
WHERE c.relkind = 'r'
AND c.relowner = r.oid
GROUP BY c.relowner;
This gives you the total disk space used by each owner, irrespective of where tables are located. It is presented as a view definition here for use below.
To make this work in a reasonably accurate fashion you need to regularly VACUUM ANALYZE
your database. If you have low traffic periods (e.g. 3am-5am daily, or Sunday) run it then using a scheduled job with user postgres. Create a function for that job that does the VACUUM and then the quota check:
CREATE FUNCTION user_quota_check() RETURNS void AS $$
DECLARE
user_data record;
BEGIN
-- Vacuum the database to get accurate disk use data
VACUUM FULL ANALYZE;
-- Find users over disk quota
FOR user_data IN SELECT * FROM user_disk_usage LOOP
IF (user_data.total_disk_usage > <<your quota>>) THEN
EXECUTE 'REVOKE CREATE ON SCHEMA ' || <<user''s schema name>> || ', PUBLIC FROM ' || user_data.rolname;
-- REVOKE INSERT privileges too, unless you work with BEFORE INSERT triggers on all tables
END IF;
END LOOP;
END; $$ LANGUAGE plpgsql;
REVOKE ALL ON FUNCTION user_quota_check() FROM PUBLIC;
If the owner goes over the quota you can REVOKE CREATE
on all relevant schemas, typically only the schema assigned to the user and the public schema, such that no new tables can be created. You should also REVOKE INSERT
on all tables but this is easily circumvented because the owner can GRANT INSERT
right back. That, however, could be cause for more drastic action against the user. Preferably you will create a before insert trigger on every table in the database, using a daily sweep just like the one above.
A user will still have SELECT privileges so he/she can still access data. More interestingly, DELETE and TRUNCATE will allow the user to free disk space and remedy the lock-out. The privileges can then be re-instated using something similar to the above function:
CREATE FUNCTION reclaim_disk_space() RETURNS void AS $$
DECLARE
disk_use bigint;
BEGIN
-- Vacuum current_user's tables.
-- Slow and therefore adequate punishment for going over quota.
VACUUM FULL VERBOSE ANALYZE;
-- Now re-instate privileges if enough space was reclaimed.
SELECT total_disk_usage INTO disk_use
FROM user_disk_usage
WHERE rolname = session_user;
IF (disk_use < <<your quota>>) THEN
EXECUTE 'GRANT CREATE ON SCHEMA ' || <<user''s schema name>> || ', PUBLIC TO ' || user_data.rolname;
-- GRANT INSERT privileges too, unless you work with BEFORE INSERT triggers on all tables
RAISE NOTICE 'Disk use under quota limit. Privileges restored.';
ELSE
RAISE NOTICE 'Still using too much disk space. Free up more space.';
END IF;
END; $$ LANGUAGE plpgsql;
The locked-out user can call this function him-/herself after having deleted sufficient data to go under the quota limit.
You can add more sophisticated features, such as having a table listing quotas per user (instead of an overall quota) and comparing actual use against that quota, issuing a RAISE NOTICE
on an insert trigger when going over 80% of quota (this requires every table to have a before insert trigger, which can easily be done by the postgres user in a regular sweep of new tables, same trigger can be used to deny inserts if over the quota), repeating that notice every hour (so record when last notice was issued), etc.
This solution is approximate because the quota are not checked in real-time. This is possible (run the user_quota_check() on every insert, modified to check just the tables of the session_user) but most likely too much overhead to make it interesting. Run user_quota_check() overnight to have daily management of quotas. And manually flog any user using up too much space during the day.