From 149d8c61407e54d3f1fb3437a1ca61235ad23b6b Mon Sep 17 00:00:00 2001 From: divyam234 <47589864+divyam234@users.noreply.github.com> Date: Fri, 2 Aug 2024 23:03:52 +0530 Subject: [PATCH] refactor: use uuid7 for fileId --- .../migrations/20240802212949_uuid.sql | 44 ++ .../migrations/20240802213957_alter_table.sql | 381 ++++++++++++++++++ pkg/models/file.go | 4 +- pkg/services/auth.go | 1 - 4 files changed, 427 insertions(+), 3 deletions(-) create mode 100644 internal/database/migrations/20240802212949_uuid.sql create mode 100644 internal/database/migrations/20240802213957_alter_table.sql diff --git a/internal/database/migrations/20240802212949_uuid.sql b/internal/database/migrations/20240802212949_uuid.sql new file mode 100644 index 0000000..7baf495 --- /dev/null +++ b/internal/database/migrations/20240802212949_uuid.sql @@ -0,0 +1,44 @@ +-- +goose Up +-- +goose StatementBegin +create or replace function public.uuid7() returns uuid as $$ +declare +begin + return uuid7(clock_timestamp()); +end $$ language plpgsql; + +create or replace function public.uuid7(p_timestamp timestamp with time zone) returns uuid as $$ +declare + + v_time double precision := null; + + v_unix_t bigint := null; + v_rand_a bigint := null; + v_rand_b bigint := null; + + v_unix_t_hex varchar := null; + v_rand_a_hex varchar := null; + v_rand_b_hex varchar := null; + + c_milli double precision := 10^3; -- 1 000 + c_micro double precision := 10^6; -- 1 000 000 + c_scale double precision := 4.096; -- 4.0 * (1024 / 1000) + + c_version bigint := x'0000000000007000'::bigint; -- RFC-9562 version: b'0111...' + c_variant bigint := x'8000000000000000'::bigint; -- RFC-9562 variant: b'10xx...' + +begin + + v_time := extract(epoch from p_timestamp); + + v_unix_t := trunc(v_time * c_milli); + v_rand_a := trunc((v_time * c_micro - v_unix_t * c_milli) * c_scale); + v_rand_b := trunc(random() * 2^30)::bigint << 32 | trunc(random() * 2^32)::bigint; + + v_unix_t_hex := lpad(to_hex(v_unix_t), 12, '0'); + v_rand_a_hex := lpad(to_hex((v_rand_a | c_version)::bigint), 4, '0'); + v_rand_b_hex := lpad(to_hex((v_rand_b | c_variant)::bigint), 16, '0'); + + return (v_unix_t_hex || v_rand_a_hex || v_rand_b_hex)::uuid; + +end $$ language plpgsql; +-- +goose StatementEnd \ No newline at end of file diff --git a/internal/database/migrations/20240802213957_alter_table.sql b/internal/database/migrations/20240802213957_alter_table.sql new file mode 100644 index 0000000..d13acf3 --- /dev/null +++ b/internal/database/migrations/20240802213957_alter_table.sql @@ -0,0 +1,381 @@ +-- +goose Up +-- +goose StatementBegin + +UPDATE teldrive.files +SET parent_id = NULL where files.parent_id = 'root'; + +ALTER TABLE teldrive.files +ADD COLUMN uuid_id UUID, +ADD COLUMN uuid_parent_id UUID; + +UPDATE teldrive.files +SET uuid_id = uuid7(files.created_at); + +UPDATE teldrive.files f1 +SET uuid_parent_id = f2.uuid_id +FROM teldrive.files f2 +WHERE f1.parent_id = f2.id; + +ALTER TABLE teldrive.files +DROP CONSTRAINT IF EXISTS files_pkey; + +ALTER TABLE teldrive.files +RENAME COLUMN id TO old_id; + +ALTER TABLE teldrive.files +RENAME COLUMN uuid_id TO id; + +ALTER TABLE teldrive.files +RENAME COLUMN parent_id TO old_parent_id; + +ALTER TABLE teldrive.files +RENAME COLUMN uuid_parent_id TO parent_id; + +ALTER TABLE teldrive.files +ADD CONSTRAINT files_pkey PRIMARY KEY (id); + +ALTER TABLE teldrive.files +ALTER COLUMN id SET DEFAULT uuid7(); + +DROP INDEX IF EXISTS teldrive.files_category_type_user_id_index; +DROP INDEX IF EXISTS teldrive.name_idx; +DROP INDEX IF EXISTS teldrive.name_search_idx; +DROP INDEX IF EXISTS teldrive.name_status_user_id_idx; +DROP INDEX IF EXISTS teldrive.parent_idx; +DROP INDEX IF EXISTS teldrive.starred_updated_at_idx; +DROP INDEX IF EXISTS teldrive.status_idx; +DROP INDEX IF EXISTS teldrive.status_user_id_idx; +DROP INDEX IF EXISTS teldrive.unique_file; +DROP INDEX IF EXISTS teldrive.unique_folder; +DROP INDEX IF EXISTS teldrive.updated_at_idx; +DROP INDEX IF EXISTS teldrive.updated_at_status_user_id_idx; +DROP INDEX IF EXISTS teldrive.user_id_idx; +DROP PROCEDURE IF EXISTS teldrive.update_size; +DROP FUNCTION IF EXISTS teldrive.move_items; +DROP FUNCTION IF EXISTS teldrive.move_directory; +DROP PROCEDURE IF EXISTS teldrive.delete_folder_recursive; +DROP PROCEDURE IF EXISTS teldrive.delete_files_bulk; +DROP FUNCTION IF EXISTS teldrive.create_directories; +DROP FUNCTION IF EXISTS teldrive.get_file_from_path; + + +CREATE INDEX idx_files_category_type_user_id ON teldrive.files USING btree (category, type, user_id); +CREATE INDEX idx_files_name ON teldrive.files USING btree (name); +CREATE INDEX idx_files_name_search ON teldrive.files USING pgroonga (regexp_replace(name, '[.,-_]'::text, ' '::text, 'g'::text)) WITH (tokenizer='TokenNgram'); +CREATE INDEX idx_files_name_user_id_status ON teldrive.files USING btree (name, user_id, status); +CREATE INDEX idx_files_parent_id ON teldrive.files USING btree (parent_id); +CREATE INDEX idx_files_starred_updated_at ON teldrive.files USING btree (starred, updated_at DESC); +CREATE INDEX idx_files_status ON teldrive.files USING btree (status); +CREATE INDEX idx_files_updated_at_user_id_status ON teldrive.files USING btree (updated_at DESC, user_id, status); +CREATE UNIQUE INDEX idx_files_unique_file ON teldrive.files USING btree (name, parent_id, user_id, size) WHERE ((status = 'active'::text) AND (type = 'file'::text)); +CREATE UNIQUE INDEX idx_files_unique_folder ON teldrive.files USING btree (name, parent_id, user_id) WHERE (type = 'folder'::text); +CREATE INDEX idx_files_updated_at ON teldrive.files USING btree (updated_at); +CREATE INDEX idx_files_user_id ON teldrive.files USING btree (user_id); + + +CREATE OR REPLACE FUNCTION teldrive.create_directories(u_id bigint, long_path text) + RETURNS SETOF teldrive.files + LANGUAGE plpgsql +AS $function$ +DECLARE + path_parts TEXT[]; + current_directory_id UUID; + new_directory_id UUID; + directory_name TEXT; + path_so_far TEXT; +BEGIN + path_parts := string_to_array(regexp_replace(long_path, '^/+', ''), '/'); + + path_so_far := ''; + + SELECT id INTO current_directory_id + FROM teldrive.files + WHERE parent_id is NULL AND user_id = u_id AND type = 'folder'; + + FOR directory_name IN SELECT unnest(path_parts) LOOP + path_so_far := CONCAT(path_so_far, '/', directory_name); + + SELECT id INTO new_directory_id + FROM teldrive.files + WHERE parent_id = current_directory_id + AND "name" = directory_name + AND "user_id" = u_id; + + IF new_directory_id IS NULL THEN + INSERT INTO teldrive.files ("name", "type", mime_type, parent_id, "user_id", starred) + VALUES (directory_name, 'folder', 'drive/folder', current_directory_id, u_id, false) + RETURNING id INTO new_directory_id; + END IF; + + current_directory_id := new_directory_id; + END LOOP; + + RETURN QUERY SELECT * FROM teldrive.files WHERE id = current_directory_id; +END; +$function$ +; + +CREATE OR REPLACE FUNCTION teldrive.get_file_from_path(full_path text, u_id bigint, throw_error boolean DEFAULT false) + RETURNS SETOF teldrive.files + LANGUAGE plpgsql +AS $function$ +DECLARE + target_id UUID; +begin + + IF full_path = '/' then + full_path := ''; + END IF; + + WITH RECURSIVE dir_hierarchy AS ( + SELECT + root.id, + root.name, + root.parent_id, + 0 AS depth, + '' as path + FROM + teldrive.files as root + WHERE + root.parent_id is NULL AND root.user_id = u_id and root.type='folder' + + UNION ALL + + SELECT + f.id, + f.name, + f.parent_id, + dh.depth + 1 AS depth, + dh.path || '/' || f.name + FROM + teldrive.files f + JOIN + dir_hierarchy dh ON dh.id = f.parent_id + WHERE f.type = 'folder' AND f.user_id = u_id + ) + + SELECT id into target_id FROM dir_hierarchy dh + WHERE dh.path = full_path + ORDER BY dh.depth DESC + LIMIT 1; + + IF throw_error IS true AND target_id IS NULL THEN + RAISE EXCEPTION 'file not found for path: %', full_path; + END IF; + + RETURN QUERY select * from teldrive.files where id=target_id; + +END; +$function$ +; + +CREATE OR REPLACE PROCEDURE teldrive.delete_files_bulk(IN file_ids text[], IN u_id bigint) + LANGUAGE plpgsql +AS $procedure$ +DECLARE + folder_ids UUID[]; +BEGIN + WITH RECURSIVE folder_tree AS ( + SELECT id + FROM teldrive.files + WHERE id = ANY (file_ids::UUID[]) + AND user_id = u_id AND type = 'folder' + + UNION ALL + + SELECT f.id + FROM teldrive.files f + JOIN folder_tree ft ON f.parent_id = ft.id + WHERE f.user_id = u_id AND f.type = 'folder' + ) SELECT array_agg(id) INTO folder_ids FROM folder_tree; + + UPDATE teldrive.files + SET status = 'pending_deletion' + WHERE (id = ANY (file_ids::UUID[]) OR parent_id = ANY (folder_ids)) + AND type = 'file' AND user_id = u_id; + + DELETE FROM teldrive.files + WHERE id = ANY (folder_ids) AND user_id = u_id; +END; +$procedure$ +; + + +CREATE OR REPLACE PROCEDURE teldrive.delete_folder_recursive(IN src text, IN u_id bigint) + LANGUAGE plpgsql +AS $procedure$ +DECLARE + folder_id UUID; + folder_ids UUID[]; +BEGIN + + IF position('/' in src) = 1 THEN + select id into folder_id from teldrive.get_file_from_path(src,u_id); + IF folder_id IS NULL THEN + RAISE EXCEPTION 'source not found'; + END IF; + ELSE + folder_id := src; + IF NOT EXISTS (SELECT 1 FROM teldrive.files WHERE id = folder_id) THEN + RAISE EXCEPTION 'source not found'; + END IF; + END IF; + + WITH RECURSIVE folder_tree AS ( + SELECT id + FROM teldrive.files + WHERE id = folder_id + AND user_id = u_id and type='folder' + + UNION ALL + + SELECT f.id + FROM teldrive.files f + JOIN folder_tree ft ON f.parent_id = ft.id + WHERE f.user_id = u_id and f.type='folder' + ) SELECT array_agg(id) INTO folder_ids FROM folder_tree; + + UPDATE teldrive.files + SET status = 'pending_deletion' + WHERE parent_id = ANY (folder_ids) and type='file' and user_id = u_id; + + DELETE FROM teldrive.files + WHERE id = ANY (folder_ids) AND user_id = u_id; +END; +$procedure$ +; + + +CREATE OR REPLACE FUNCTION teldrive.move_directory(src text, dest text, u_id bigint) + RETURNS void + LANGUAGE plpgsql +AS $function$ +DECLARE + src_parent TEXT; + src_base TEXT; + dest_parent TEXT; + dest_base TEXT; + dest_id UUID; + dest_parent_id UUID; + src_id UUID; +begin + + select id into src_id from teldrive.get_file_from_path(src,u_id); + + select id into dest_id from teldrive.get_file_from_path(dest,u_id); + + IF src_id is NULL THEN + RAISE EXCEPTION 'source directory not found'; + END IF; + + IF dest_id is not NULL then + RAISE EXCEPTION 'destination directory exists'; + END IF; + + SELECT parent, base INTO src_parent,src_base FROM teldrive.split_path(src); + + SELECT parent, base INTO dest_parent, dest_base FROM teldrive.split_path(dest); + + IF src_parent != dest_parent then + select id into dest_id from teldrive.create_directories(u_id,dest); + UPDATE teldrive.files SET parent_id = dest_id WHERE parent_id = src_id; + delete from teldrive.files where id = src_id; + END IF; + + IF src_base != dest_base and src_parent = dest_parent then + UPDATE teldrive.files SET name = dest_base WHERE id = src_id; + END IF; + +END; +$function$ +; + + +CREATE OR REPLACE FUNCTION teldrive.move_items(file_ids text[], dest text, u_id bigint) + RETURNS void + LANGUAGE plpgsql +AS $function$ +declare +dest_id UUID; +BEGIN + + select id into dest_id from teldrive.get_file_from_path(dest,u_id); + + IF dest_id is NULL then + select id into dest_id from teldrive.create_directories(u_id,dest); + END IF; + + UPDATE teldrive.files + SET parent_id = dest_id + WHERE id = ANY(file_ids::UUID[]); +END; +$function$ +; + +CREATE OR REPLACE PROCEDURE teldrive.update_size() + LANGUAGE plpgsql +AS $procedure$ +begin + + WITH RECURSIVE folder_hierarchy AS ( + + SELECT + id, + name, + parent_id, + ARRAY[id] AS path + FROM + teldrive.files + WHERE + type = 'folder' AND parent_id is NULL + + UNION ALL + + SELECT + f.id, + f.name, + f.parent_id, + fh.path || f.id + FROM + teldrive.files f + JOIN + folder_hierarchy fh ON f.parent_id = fh.id + WHERE + f.type = 'folder' + ) + , folder_sizes AS ( + SELECT + f.id, + f.path, + COALESCE(SUM(CASE WHEN c.type != 'folder' THEN c.size ELSE 0 END), 0) AS direct_size + FROM + folder_hierarchy f + LEFT JOIN + teldrive.files c ON f.id = c.parent_id + GROUP BY + f.id, f.path + ) + , cumulative_sizes AS ( + SELECT + fs.id, + SUM(fs2.direct_size) AS total_size + FROM + folder_sizes fs + JOIN + folder_sizes fs2 ON fs2.path @> fs.path + GROUP BY + fs.id + ) + UPDATE teldrive.files f + SET size = cs.total_size + FROM cumulative_sizes cs + WHERE f.id = cs.id AND f.type = 'folder'; +END; +$procedure$ +; + +ALTER TABLE teldrive.files +DROP COLUMN old_id, +DROP COLUMN old_parent_id; +DROP FUNCTION IF EXISTS teldrive.generate_uid; +-- +goose StatementEnd \ No newline at end of file diff --git a/pkg/models/file.go b/pkg/models/file.go index e8b700b..70351ef 100644 --- a/pkg/models/file.go +++ b/pkg/models/file.go @@ -8,7 +8,7 @@ import ( ) type File struct { - Id string `gorm:"type:text;primaryKey;default:generate_uid(16)"` + Id string `gorm:"type:uuid;primaryKey;default:uuid7()"` Name string `gorm:"type:text;not null"` Type string `gorm:"type:text;not null"` MimeType string `gorm:"type:text;not null"` @@ -18,7 +18,7 @@ type File struct { Encrypted bool `gorm:"default:false"` UserID int64 `gorm:"type:bigint;not null"` Status string `gorm:"type:text"` - ParentID string `gorm:"type:text;index"` + ParentID string `gorm:"type:uuid;index"` Parts datatypes.JSONSlice[schemas.Part] `gorm:"type:jsonb"` ChannelID *int64 `gorm:"type:bigint"` CreatedAt time.Time `gorm:"default:timezone('utc'::text, now())"` diff --git a/pkg/services/auth.go b/pkg/services/auth.go index ed38db0..24ad9f0 100644 --- a/pkg/services/auth.go +++ b/pkg/services/auth.go @@ -94,7 +94,6 @@ func (as *AuthService) LogIn(c *gin.Context, session *schemas.TgSession) (*schem MimeType: "drive/folder", UserID: session.UserID, Status: "active", - ParentID: "root", } if err := as.db.Clauses(clause.OnConflict{DoNothing: true}).Create(file).Error; err != nil { return err