Bivio::Biz::Model::RealmFile
# Copyright (c) 2005-2011 bivio Software, Inc. All Rights Reserved. # $Id$ package Bivio::Biz::Model::RealmFile; use strict; use Bivio::Base 'Biz.PropertyModel'; use Bivio::IO::Trace; our($_TRACE); my($_IDI) = __PACKAGE__->instance_data_index; my($_BF) = b_use('Biz.File'); my($_BFN) = b_use('Type.BlogFileName'); my($_DELETED_SENTINEL) = 'DELETED IN TRANSACTION'; my($_DFN) = b_use('Type.DocletFileName'); my($_DT) = b_use('Type.DateTime'); my($_FP) = b_use('Type.FilePath'); my($_IOF) = b_use('IO.File'); my($_FWQ) = b_use('Biz.FailoverWorkQueue'); my($_T) = b_use('MIME.Type'); my($_WN) = b_use('Type.WikiName'); my($_TXN_PREFIX); my($_TXN_FILE_PATTERN_RE) = qr{(?:^|/)\..+\#[^/]+$}s; my($_S) = b_use('Bivio.Search'); my($_VERSIONS_FOLDER) = $_FP->VERSIONS_FOLDER; my($_VERSION_REGEX) = $_FP->VERSION_REGEX; #DEPRECATED sub MAIL_FOLDER { # Always is_read_only => 1 return $_DFN->MAIL_FOLDER; } #DEPRECATED sub PUBLIC_FOLDER { # Always is_public => 1 return $_DFN->PUBLIC_FOLDER_ROOT; } sub TXN_FILE_PATTERN_RE { return $_TXN_FILE_PATTERN_RE } sub append_content { my($self, $content) = @_; #TODO: Optimize to only append the file. return $self->update_with_content({ user_id => $self->get('user_id'), }, \(${$self->get_content} . $$content)); } sub copy_deep { Bivio::IO::Alert->warn_deprecated('use unauth_copy_deep()'); return unauth_copy_deep(@_); } sub create { my($self, $values) = @_; my($v) = {%$values}; my($c) = delete($v->{_content}); _create($self, $v); return $v->{is_folder} ? $self : _write($self, defined($c) ? $c : \('')); } sub create_folder { my($self, $values) = @_; return shift->create({ %$values, is_folder => 1, }); } sub create_or_update_with_content { my($self, $values) = _with_content(@_); return $self->create_or_update($values); } sub create_or_update_with_file { return _with_file(shift, 'create_or_update', shift, shift); } sub create_with_content { my($self, $values) = _with_content(@_); return $self->create($values); } sub create_with_file { return _with_file(shift, 'create', shift, shift); } sub delete { my($self, $values) = _delete_args(@_); return 0 unless $self; $self->throw_die(FORBIDDEN => { entity => $self->get('path'), message => 'folder is not empty', }) unless $self->is_empty; return _delete_one($self, $values); } sub delete_all { my($self, $query) = @_; $self = $self->new unless $self->is_instance; my($req) = $self->get_request; my($realm); if ($query && $query->{realm_id}) { Bivio::IO::Alert->warn_deprecated( 'ignoring specified realm_id (', $query->{realm_id}, ') because it does not match auth_id (', $req->get('auth_id'), '). Using the auth_id instead.', ) if $query->{realm_id} != $req->get('auth_id'); $realm = $req->get('auth_realm'); delete($query->{realm_id}); } $self->die('unsupported with a query: ', $query) if $query && %$query; my($op) = sub { my($d) = _realm_dir($req->get('auth_id')); _txn($self, _search_delete($self, [delete => glob("$d/[0-9]*[0-9]")])); return $self->SUPER::delete_all({ realm_id => $req->get('auth_id'), }); }; return $realm ? $req->with_realm($realm, $op) : $op->(); } sub delete_deep { Bivio::IO::Alert->warn_deprecated('use unauth_delete_deep()'); return shift->unauth_delete_deep(@_); } sub delete_empty_folders { my($self) = @_; my($rf) = $self->new_other('RealmFile')->set_ephemeral; while (1) { my($folders) = b_use('Type.PrimaryIdArray') ->from_literal( $rf->map_iterate( sub {shift->get('realm_file_id')}, {is_folder => 1}, ), ); last if $folders->as_length <= 1; my($to_delete) = $folders->exclude( b_use('Biz.ListModel') ->new_anonymous({ primary_key => ['RealmFile.folder_id'], want_select_distinct => 1, #TODO: ignore_model_primary_keys => 1, other => [ { name => 'RealmFile.realm_file_id', in_select => 0, }, ], auth_id => 'RealmFile.realm_id', })->map_iterate( sub {shift->get('RealmFile.folder_id')}, ), ); last if $to_delete->as_length <= 0; $to_delete->do_iterate(sub { $rf->delete({realm_file_id => shift}); return 1; }); } return; } sub get_content { return _read(_filename(@_)); } sub get_content_length { return -s shift->get_os_path(@_); } sub get_content_type { my($proto, undef, $prefix, $values) = shift->internal_get_target(@_); if ($values->{$prefix . 'is_folder'}) { Bivio::IO::Alert->warn_deprecated('check is_folder first'); return ''; } return $proto->get_content_type_for_path($values->{$prefix . 'path'}); } sub get_content_type_for_path { my(undef, $path) = @_; $path =~ s{@{[$_FP->VERSION_REGEX]}}{}; my($res) = $_T->from_extension($path); return $res eq 'application/octet-stream' && ($_WN->is_absolute($path) || $_BFN->is_absolute($path)) ? 'text/x-bivio-wiki' : $res; } sub get_handle { my($self) = shift; my($os_path) = $self->get_os_path(@_); return IO::File->new($os_path, 'r') || ($self->internal_get_target(@_))[1]->throw_die(IO_ERROR => { entity => $os_path, message => "$!", }); } sub get_os_path { # Use with caution: May be transaction file or actual file return _os_path(_filename(@_)); } sub handle_commit { return _txn_do( shift(@_), sub { my($file, $txn_file) = @_; return unless -r $txn_file; _trace('rename(', $txn_file, ', ', $file, ')') if $_TRACE; unlink($file); $_IOF->rename($txn_file, $file); $_FWQ->create_file($file); return; }, sub { my($file, $txn_file) = @_; _trace('unlink(', $txn_file, ', ', $file, ')') if $_TRACE; unlink($file); unlink($txn_file); $_FWQ->delete_file($file); return; } ); } sub handle_rollback { return _txn_do( shift(@_), sub { my(undef, $txn_file) = @_; unlink($txn_file); return; }, ); } sub init_realm { my($self) = shift; $self->die(DIE => { entity => \@_, message => 'init_realm must be called from within realm, use $req->with_realm', }) if @_; my($v) = { path => '/', realm_id => $self->req('auth_id'), }; return $self if $self->unsafe_load($v); return $self->create_folder($v); } sub internal_clear_model_cache { my($self) = @_; $self->[$_IDI] = undef; return shift->SUPER::internal_clear_model_cache(@_); } sub internal_initialize { my($self) = @_; return $self->merge_initialize_info($self->SUPER::internal_initialize, { version => 1, table_name => 'realm_file_t', as_string_fields => [qw(realm_id path)], columns => { realm_file_id => ['PrimaryId', 'PRIMARY_KEY'], realm_id => ['RealmOwner.realm_id', 'NOT_NULL'], folder_id => ['PrimaryId', 'NONE'], # Don't cascade when User.user_id is deleted user_id => ['PrimaryId', 'NOT_NULL'], modified_date_time => ['DateTime', 'NOT_NULL'], is_folder => ['Boolean', 'NOT_NULL'], is_public => ['Boolean', 'NOT_NULL'], is_read_only => ['Boolean', 'NOT_NULL'], path => ['FilePath', 'NOT_NULL'], path_lc => ['FilePath', 'NOT_NULL'], }, other => [ [qw(realm_id RealmOwner.realm_id)], [qw(user_id User.user_id)], ], auth_id => 'realm_id', }); } sub internal_prepare_query { my($self, $query) = @_; foreach my $k (keys(%{_child_attrs($query)})) { delete($query->{$k}); } return shift->SUPER::internal_prepare_query(@_) if ref($query->{path_lc}); # Only load by path_lc, and convert from_literal (which is idempotent) if (exists($query->{path})) { my($p) = delete($query->{path}); $query->{path_lc} = $p unless exists($query->{path_lc}); } if (exists($query->{path_lc})) { # The value won't be found if it is illegal; Don't call parse_path my($p, $e) = $_FP->from_literal($query->{path_lc}); _trace($query, ': path error ', $e) if $e && $_TRACE; $query->{path_lc} = lc($p) if $p; } _trace($query) if $_TRACE; return shift->SUPER::internal_prepare_query(@_); } sub internal_unique_load_values { my($self, $values) = @_; return { map(($_ => $values->{$_} || return), 'realm_id', (grep($values->{$_}, qw(path_lc path)))[0] || return, ), }; } sub is_empty { _assert_loaded(@_); my($self) = @_; return 1 unless $self->get('is_folder'); my($got_one) = 0; $self->new_other('RealmFileList') ->set_ephemeral ->do_iterate( sub {$got_one++}, { auth_id => $self->get('realm_id'), path_info => $self->get('path') }, ); return $got_one ? 0 : 1; } sub is_public { return _path(@_) =~ m{^\Q@{[$_FP->PUBLIC_FOLDER]}\E(?:/|$)}i ? 1 : 0; } sub is_backup { return _path(@_) =~ m{[\~\%\#\$]|/\.|\.bak$|-$}i ? 1 : 0; } sub is_mail { return _path(@_) =~ m{^\Q@{[$_FP->MAIL_FOLDER]}\E(?:/|$)}i ? 1 : 0; } sub is_searchable { my($self) = @_; return $self->get('is_folder') || $self->is_version || $self->is_backup ? 0 : 1; } sub is_text_content_type { return shift->get_content_type(@_) =~ m{^(?:text/|application/x-perl)} ? 1 : 0; } sub is_text_file { my($self) = @_; return -T $self->get_os_path ? 1 : 0; } sub is_version { return _path(@_) =~ m{^\Q@{[$_FP->VERSIONS_FOLDER]}\E(?:/|$)}i ? 1 : 0; } sub parse_path { my($proto, $path, $model) = @_; my($p, $e) = $_FP->from_literal(defined($path) ? $path : '/'); ($model || $proto)->throw_die( CORRUPT_QUERY => { message => 'invalid path', type_error => $e, entity => $path, }, ) if $e; return $p ? $p : '/'; } sub path_info_to_id { my($self, $path_info) = @_; return $self->load({path => $self->parse_path($path_info)}) ->get('realm_file_id'); } sub restore { _assert_loaded(@_); my($self) = @_; my($old_path) = $self->get('path'); my($new_path) = $self->restore_path; $self->throw_die(INVALID_OP => 'attempt to restore non-archived file') unless $new_path; my($rf) = $self->new_other('RealmFile')->set_ephemeral; if ($rf->load({ path => $old_path, })->get('is_folder')) { $self->throw_die(INVALID_OP => 'may not restore existing folders') if $rf->unsafe_load({ path => $new_path, }); $rf->create({ path => $new_path, is_folder => 1, }); my($restored) = {}; $self->new_other('RealmFileList') ->set_ephemeral ->do_iterate( sub { my($rf) = shift->get_model('RealmFile'); my($rp) = $rf->restore_path; # Only restore the latest version of each file return 1 if $restored->{$rp}; $rf->restore; $restored->{$rp} = 1; return 1; }, { path_info => $old_path, order_by => ['RealmFile.path_lc', 'desc'], }, ); } else { $self->new_other('RealmFile') ->set_ephemeral ->create_or_update_with_file({ path => $new_path, }, $old_path); } return; } sub restore_path { _assert_loaded(@_); my($self) = @_; my($archive_path) = $self->get('path'); return undef unless $archive_path =~ s/$_VERSIONS_FOLDER//; $archive_path =~ s/$_VERSION_REGEX//; return $archive_path; } sub toggle_is_public { my($self) = @_; my($ip) = $self->get('is_public') ? 0 : 1; my($method) = $ip ? 'to_public' : 'from_public'; $self->update({ override_is_read_only => 1, is_public => $ip, path => $_FP->$method($self->get('path')), modified_date_time => $self->get('modified_date_time'), }); return; } sub unauth_copy_deep { my($self, $dest) = @_; #TODO: Die if $dest->is_version my($size) = 0; return _copy( $self, { %{_copy_attrs($self)}, map(exists($dest->{$_}) ? ($_ => $dest->{$_}) : (), qw(path realm_id user_id is_read_only is_public)), }, undef, \$size, ); } sub unauth_delete { # We don't support this to avoid the 'rm -rf /' problem that Unix has. # It's technically feasible, but not something you ever want to do. die('unsupported'); } sub unauth_delete_deep { my($self, $values) = _delete_args(@_); return 0 unless $self; return _delete_one($self, $values) unless $self->get('is_folder'); my($count) = 0; my($v) = $self->get_shallow_copy; foreach my $child (@{ $self->new_other('RealmFileList') ->set_ephemeral ->map_iterate( sub {shift->get_model('RealmFile')}, unauth_iterate_start => { auth_id => $v->{realm_id}, path_info => $v->{path}, }, ), }) { $count += $child->unauth_delete_deep(_child_attrs($values, $self)); } return $count +_delete_one($self, $values) } sub update { _assert_loaded(@_); my($self, $new_values) = @_; $self->throw_die(INVALID_OP => 'may not modify "is_folder"') if exists($new_values->{is_folder}) && $self->get('is_folder') ne $new_values->{is_folder}; $self->throw_die(FORBIDDEN => 'may not change root path') if $self->get('path') eq '/' && exists($new_values->{path}) && ($new_values->{path} || 'invalid path') ne '/'; $self->throw_die(FORBIDDEN => 'public files must live under /Public') if $new_values->{is_public} && ($new_values->{path} || $self->get('path')) !~ m{^@{[$self->PUBLIC_FOLDER]}($|/)}oi; #TODO: Die if new path->is_version return _update($self, { map(($_ => $self->get($_)), qw(is_folder path realm_id is_public is_read_only)), user_id => $self->get_request->get('auth_user_id') || $self->get('user_id'), %$new_values, }); } sub update_with_content { my($self, $values) = _with_content(@_); return $self->update($values); } sub update_with_file { return _with_file(shift, 'update', shift, shift); } sub _assert_loaded { my($self) = @_; $self->die('not loaded') unless $self->is_loaded; return; } sub _assert_not_root { my($self) = @_; $self->throw_die(FORBIDDEN => 'cannot perform operation on root') if $self->get('path') eq '/'; return; } sub _assert_writable { my($self, $values) = @_; $self->throw_die(FORBIDDEN => 'file or folder is read-only') if $self->unsafe_get('is_read_only') && !$values->{override_is_read_only}; return; } sub _child_attrs { my($v, $parent) = @_; return { map(($_ => $v->{$_}), grep(/^_|override/, keys(%$v))), $parent ? (_parent => $parent) : (), }; } sub _copy { my($self, $values, $size) = @_; my($dst) = $self->new; _assert_writable($dst, $values); $dst->unauth_create_or_update({ %{_verify_and_fix($dst, $values)}, $self->get('is_folder') ? () : (_content => $self->get_content), }); _trace($self, ' -> ', $dst, '=', $dst->get_shallow_copy) if $_TRACE; return unless $dst->get('is_folder'); my($old_length) = length($self->get('path')); foreach my $src (@{ $self->new_other('RealmFileList')->map_iterate( sub {shift->get_model('RealmFile')}, unauth_iterate_start => { auth_id => $self->get('realm_id'), path_info => $self->get('path'), }, ), }) { # Allow copy of a single file of any size above, but cummulative # copies have to blow up at some point. $src->throw_die(NO_RESOURCES => {message => 'copy too large'}) if ($$size += $src->get_content_length || 0) > 30_000_000; _copy( $src, { %{_copy_attrs($src)}, %{_child_attrs($values, $dst)}, map(($_ => $dst->get($_)), qw(realm_id user_id)), path => $dst->get('path') . substr($src->get('path'), $old_length), }, $size, ); } return; } sub _copy_attrs { my($self) = @_; return { @{$self->map_each( sub { my(undef, $k, $v) = @_; return grep( $k =~ /$_/, qw(is_read_only is_public realm_file_id), ) ? () : ($k =~ /(\w+)$/, $v); }, )}, }; } sub _create { my($self, $values) = @_; my($req) = $self->get_request; $values->{realm_id} ||= $req->get('auth_id'); $values->{user_id} ||= $req->get('auth_user_id'); $self->internal_unload; my($v) = { $values->{path} eq '/' ? (is_public => 0, is_read_only => 0) : (), %{_verify_and_fix($self, $values)}, }; _trace($v) if $_TRACE; return $self->SUPER::create($v); } sub _delete_one { my($self, $values) = @_; _trace($self) if $_TRACE; return $self->SUPER::delete if $self->get('is_folder'); my($p) = $self->get('path'); if ($values->{override_versioning} || $self->is_version) { $self->SUPER::delete; _txn($self, _search_delete($self, [delete => _filename($self)])); } else { my($p) = $_FP->join($_FP->VERSIONS_FOLDER, $p); $self->clone->update({ path => _next_version($self->get('realm_id'), $p), modified_date_time => $self->get('modified_date_time'), override_is_read_only => 1, }); } return 1; } sub _delete_args { my($self, $values) = @_; my($load_args) = _non_child_attrs($values || {}); ($self = $self->new)->unsafe_load($values) if %$load_args; return unless $self->is_loaded; _assert_not_root($self); _assert_writable($self, $values); return ($self, _verify_and_fix( $self, { %{$self->get_shallow_copy}, %{_child_attrs($values)}, }, ), ); } sub _filename { my(undef, $model, $prefix, $values) = shift->internal_get_target(@_); my($d, $f) = map($values->{"$prefix$_"}, qw(realm_id realm_file_id)); my($res) = _realm_dir($d) . '/' . $f; _trace($res) if $_TRACE; return $res; } sub _next_version { my($rid, $p) = @_; my($base) = lc($p); my($suffix) = $_FP->get_suffix($base); if (length($suffix)) { $suffix = ".$suffix"; substr($base, -length($suffix)) = ''; } my($max) = 0; Bivio::SQL::Connection->do_execute( sub { my($v) = shift->[0] =~ /^\Q$base\E;(\d+)\Q$suffix\E$/s; $max = $v if defined($v) && $v > $max; return 1; }, q{SELECT path_lc FROM realm_file_t WHERE realm_id = ? AND SUBSTR(path_lc, 1, LENGTH(?) + 1) = ? AND STRPOS(SUBSTR(path_lc, LENGTH(?) + 2), '/') = 0}, [$rid, $base, $base . ';', $base]); substr($p, length($base), 0) = ';' . ++$max; return $p; } sub _non_child_attrs { my($v) = @_; return {map(($_ => $v->{$_}), grep(!/^_|override/, keys(%$v)))}; } sub _os_path { my($file) = @_; my($txn_file) = _txn_filename($file); Bivio::Die->die(IO_ERROR => { entity => $file, message => 'file has been deleted in this transaction', }) if -l $txn_file && -e $txn_file; return -r $txn_file ? $txn_file : $file; } sub _path { my($self, $v) = @_; return defined($v) ? $v : $self->get('path'); } sub _read { return $_IOF->read(_os_path(shift(@_))); } sub _realm_dir { my($realm_id) = @_; return $_BF->absolute_path("RealmFile/$realm_id"); } sub _search_delete { my($self, $cmds) = @_; $_S->map_invoke( 'delete_model', [map(/(\w+)$/, @$cmds[1..$#$cmds])], [$self->req], ); return $cmds; } sub _search_update { my($self) = @_; $_S->update_model($self->req, $self); return $self; } sub _touch_parent { my($self, $values) = @_; return if $values->{_touch_parent} || $values->{path} eq '/'; my($parent) = $self->new_other->set_ephemeral; my($parent_path) = ($values->{path} =~ m{(^/.+)/})[0] || '/'; return $parent->create_folder({ map(($_ => $values->{$_}), qw(user_id realm_id override_is_read_only)), path => $parent_path, }) unless $parent->unauth_load({ realm_id => $values->{realm_id}, path => $parent_path, }); $parent->throw_die(IO_ERROR => { entity => $values->{path}, message => 'parent exists as a file, but must be a folder', }) unless $parent->get('is_folder'); # match case of folder that exists substr($values->{path}, 0, length($parent->get('path'))) = $parent->get('path'); if ($values->{_update}) { return $parent unless $self->get('path') ne $values->{path} || $self->get('realm_id') ne $values->{realm_id}; } # touch director(ies); also asserts writable my($v) = _child_attrs($values); delete($v->{_parent}); delete($v->{_update}); _touch_parent( $self, {map(($_ => $self->get($_)), qw(realm_id path)), %$v}, ) if $values->{_update}; return $parent->update({%$v, _touch_parent => 1}); } sub _txn { my($self) = shift; # Need to create $new, because callers may modify or re-use $self after call my($new) = $self->new; $new->[$_IDI] = [@_]; $new->get_request->push_txn_resource($new); return _txn_do( $new, sub { my($file, $txn_file, $content) = @_; $_IOF->mkdir_parent_only($txn_file); unlink($txn_file); $_IOF->write($txn_file, $content); return; }, sub { my($file, $txn_file) = @_; $_IOF->mkdir_parent_only($txn_file); unlink($txn_file); symlink($_DELETED_SENTINEL, $txn_file); return; }, ); } sub _txn_do { my($self, $create, $delete) = @_; return unless ref($self) and my $cmds = $self->[$_IDI]; $delete ||= $create; foreach my $cmd (@$cmds) { my($op, @args) = @$cmd; if ($op eq 'create') { # First time we get rid of content, which may be large. pop(@$cmd) if $cmd->[2]; $create->($args[0], _txn_filename($args[0]), $args[1]); } elsif ($op eq 'delete') { foreach my $f (@args) { $delete->($f, _txn_filename($f)); } } else { Bivio::Die->die($cmd, ': program error'); } } return; } sub _txn_filename { my($filename) = @_; $_TXN_PREFIX ||= '.' . $_IOF->unique_name_for_process . '#'; $filename =~ s{(?=[^/]+$)}{$_TXN_PREFIX}o; return $filename; } sub _update { my($self, $values) = @_; _assert_writable($self, $values); my($c) = delete($values->{_content}); my($method) = 'SUPER::update'; my($versioned) = $c && _version($self, { %{$self->get_shallow_copy}, $values->{override_versioning} ? (override_versioning => 1) : (), }); if ($versioned) { $method = 'SUPER::create'; delete($values->{realm_file_id}); } my($old_realm) = $self->get('realm_id'); my($old_filename) = _filename($self); my($old_path) = $self->get('path'); $values->{path} = $values->{_parent}->get('path') . substr($self->get('path'), $values->{old_path_length}) if $values->{_parent} && $values->{old_path_length}; $self->$method( _verify_and_fix($self, { realm_id => $old_realm, path => $old_path, %$values, _update => 1, })); _trace($old_realm, ', ', $old_path, ' -> ', $self->get_shallow_copy) if $_TRACE; my($new_filename) = _filename($self); unless ($self->get('is_folder')) { _txn($self, # delete must come first for search to work right [delete => $old_filename], $c ? () : [create => $new_filename, _read($old_filename)], ) unless $versioned || $new_filename eq $old_filename; return defined($c) ? _write($self, $c) : _search_update($self); } my($new_path) = $self->get('path'); return $self if $old_realm eq $self->get('realm_id') && $old_path eq $new_path; foreach my $child (@{ $self->new_other('RealmFileList') ->set_ephemeral ->map_iterate( sub {shift->get_model('RealmFile')}, unauth_iterate_start => { auth_id => $old_realm, path_info => $old_path, }, ), }) { _update($child, { %{$child->get_shallow_copy}, realm_id => $self->get('realm_id'), %{_child_attrs($values, $self)}, old_path_length => length($old_path), }); } return $self; } sub _verify { my($self, $values) = @_; my($p) = $values->{path_lc} = lc($values->{path} = $self->parse_path($values->{path})); $values->{is_read_only} = 1 if $self->is_version($p) || $self->is_mail($p); $values->{is_public} = $self->is_public($p) ? 1 : 0; $values->{modified_date_time} ||= $_DT->now; return $values; } sub _verify_and_fix { my($self, $values) = @_; $values = _verify($self, {%$values}); return $values unless $values->{_parent} ||= _touch_parent($self, $values); foreach my $k (qw(is_public is_read_only)) { $values->{$k} = $values->{_parent}->get($k) unless exists($values->{$k}); } $values->{folder_id} = $values->{_parent}->get('realm_file_id'); _trace($values) if $_TRACE; return $values; } sub _version { my($self, $values) = @_; return $self->new->delete({ %$values, override_is_read_only => 1, is_folder => 0, realm_file_id => $self->get('realm_file_id'), _update => 1, }); } sub _with_content { my($self, $values, $content) = @_; return ($self, { $values ? %$values : (), is_folder => 0, _content => ref($content) ? $content : \$content, }); } sub _with_file { my($self, $method, $values, $id_or_path) = @_; $self->die('must provide a method prefix') unless defined($method) && length($method); $method .= '_with_content'; return $self->$method( $values, $self->new_other('RealmFile')->set_ephemeral->load({ $id_or_path =~ /^\d+$/ ? (realm_file_id => $id_or_path) : (path => $id_or_path), })->get_content, ); } sub _write { my($self, $content) = @_; $self->die('cannot put content on a directory') if $self->get('is_folder'); $self->throw_die(DIE => { entity => $content, message => 'content must be a defined scalar_ref', }) unless ref($content) eq 'SCALAR' && defined($$content); _txn($self, [create => _filename($self), $content]); return _search_update($self); } 1;