FlatFile.pm 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package Trog::Data::FlatFile;
  2. use strict;
  3. use warnings;
  4. no warnings 'experimental';
  5. use feature qw{signatures};
  6. use Carp qw{confess};
  7. use JSON::MaybeXS;
  8. use File::Slurper;
  9. use File::Copy;
  10. use Mojo::File;
  11. use lib 'lib';
  12. use Trog::SQLite::TagIndex;
  13. use parent qw{Trog::DataModule};
  14. our $datastore = 'data/files';
  15. sub lang { 'Perl Regex in Quotemeta' }
  16. sub help { 'https://perldoc.perl.org/functions/quotemeta.html' }
  17. =head1 Trog::Data::FlatFile
  18. This data model has multiple drawbacks, but is "good enough" for most low-content and few editor applications.
  19. You can only post once per second due to it storing each post as a file named after the timestamp.
  20. =cut
  21. our $parser = JSON::MaybeXS->new( utf8 => 1 );
  22. sub read ($self, $query={}) {
  23. $query->{limit} //= 25;
  24. #Optimize direct ID
  25. my @index;
  26. if ($query->{id}) {
  27. @index = ("$datastore/$query->{id}");
  28. } else {
  29. if (-f 'data/posts.db') {
  30. @index = map { "$datastore/$_" } Trog::SQLite::TagIndex::posts_for_tags(@{$query->{tags}})
  31. }
  32. @index = $self->_index() unless @index;
  33. }
  34. my @items;
  35. foreach my $item (@index) {
  36. next unless -f $item;
  37. my $slurped = eval { File::Slurper::read_binary($item) };
  38. if (!$slurped) {
  39. print "Failed to Read $item:\n$@\n";
  40. next;
  41. }
  42. my $parsed = eval { $parser->decode($slurped) };
  43. if (!$parsed) {
  44. print "JSON Decode error on $item:\n$@\n";
  45. next;
  46. }
  47. #XXX this imposes an inefficiency in itself, get() will filter uselessly again here
  48. my @filtered = $self->filter($query,@$parsed);
  49. push(@items,@filtered) if @filtered;
  50. next if $query->{limit} == 0; # 0 = unlimited
  51. last if scalar(@items) == $query->{limit};
  52. }
  53. return \@items;
  54. }
  55. sub _index ($self) {
  56. confess "Can't find datastore!" unless -d $datastore;
  57. opendir(my $dh, $datastore) or confess;
  58. my @index = grep { -f } map { "$datastore/$_" } readdir $dh;
  59. closedir $dh;
  60. return sort { $b cmp $a } @index;
  61. }
  62. sub write($self,$data) {
  63. foreach my $post (@$data) {
  64. my $file = "$datastore/$post->{id}";
  65. my $update = [$post];
  66. if (-f $file) {
  67. my $slurped = File::Slurper::read_binary($file);
  68. my $parsed = $parser->decode($slurped);
  69. $update = [(@$parsed, $post)];
  70. }
  71. open(my $fh, '>', $file) or confess;
  72. print $fh $parser->encode($update);
  73. close $fh;
  74. Trog::SQLite::TagIndex::add_post($post,$self);
  75. }
  76. }
  77. sub count ($self) {
  78. my @index = $self->_index();
  79. return scalar(@index);
  80. }
  81. sub add ($self,@posts) {
  82. my $ctime = time();
  83. @posts = map {
  84. $_->{id} //= $ctime;
  85. $_->{created} = $ctime;
  86. $_
  87. } @posts;
  88. return $self->SUPER::add(@posts);
  89. }
  90. sub delete($self, @posts) {
  91. foreach my $update (@posts) {
  92. unlink "$datastore/$update->{id}" or confess;
  93. Trog::SQLite::TagIndex::remove_post($update);
  94. }
  95. return 0;
  96. }
  97. 1;