FlatFile.pm 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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. # Initialize the list of posts by tag for all known tags.
  23. # This is because the list won't ever change between HUPs
  24. our @tags = Trog::SQLite::TagIndex::tags();
  25. our %posts_by_tag;
  26. sub read ($self, $query={}) {
  27. $query->{limit} //= 25;
  28. #Optimize direct ID
  29. my @index;
  30. if ($query->{id}) {
  31. @index = ("$datastore/$query->{id}");
  32. } else {
  33. # Remove tags which we don't care about and sort to keep memoized memory usage down
  34. @{$query->{tags}} = sort grep { my $t = $_; grep { $t eq $_ } @tags } @{$query->{tags}};
  35. my $tagkey = join('&',@{$query->{tags}});
  36. # Check against memoizer
  37. $posts_by_tag{$tagkey} //= [];
  38. @index = @{$posts_by_tag{$tagkey}} if @{$posts_by_tag{$tagkey}};
  39. if (!@index && -f 'data/posts.db') {
  40. @index = map { "$datastore/$_" } Trog::SQLite::TagIndex::posts_for_tags(@{$query->{tags}});
  41. $posts_by_tag{$tagkey} = \@index;
  42. }
  43. @index = $self->_index() unless @index;
  44. }
  45. my @items;
  46. foreach my $item (@index) {
  47. next unless -f $item;
  48. my $slurped = eval { File::Slurper::read_text($item) };
  49. if (!$slurped) {
  50. print "Failed to Read $item:\n$@\n";
  51. next;
  52. }
  53. my $parsed = eval { $parser->decode($slurped) };
  54. if (!$parsed) {
  55. # Try and read it in binary in case it was encoded incorrectly the first time
  56. $slurped = eval { File::Slurper::read_binary($item) };
  57. $parsed = eval { $parser->decode($slurped) };
  58. if (!$parsed) {
  59. print "JSON Decode error on $item:\n$@\n";
  60. next;
  61. }
  62. }
  63. #XXX this imposes an inefficiency in itself, get() will filter uselessly again here
  64. my @filtered = $query->{raw} ? @$parsed : $self->filter($query,@$parsed);
  65. push(@items,@filtered) if @filtered;
  66. next if $query->{limit} == 0; # 0 = unlimited
  67. last if scalar(@items) == $query->{limit};
  68. }
  69. return \@items;
  70. }
  71. sub _index ($self) {
  72. confess "Can't find datastore in $datastore !" unless -d $datastore;
  73. opendir(my $dh, $datastore) or confess;
  74. my @index = grep { -f } map { "$datastore/$_" } readdir $dh;
  75. closedir $dh;
  76. return sort { $b cmp $a } @index;
  77. }
  78. sub routes ($self) {
  79. return Trog::SQLite::TagIndex::routes();
  80. }
  81. sub aliases ($self) {
  82. return Trog::SQLite::TagIndex::aliases();
  83. }
  84. sub write($self,$data) {
  85. foreach my $post (@$data) {
  86. my $file = "$datastore/$post->{id}";
  87. my $update = [$post];
  88. if (-f $file) {
  89. my $slurped = File::Slurper::read_binary($file);
  90. my $parsed = $parser->decode($slurped);
  91. $update = [(@$parsed, $post)];
  92. }
  93. mkdir $datastore;
  94. open(my $fh, '>', $file) or confess "Could not open $file";
  95. print $fh $parser->encode($update);
  96. close $fh;
  97. Trog::SQLite::TagIndex::add_post($post,$self);
  98. }
  99. }
  100. sub count ($self) {
  101. my @index = $self->_index();
  102. return scalar(@index);
  103. }
  104. sub delete($self, @posts) {
  105. foreach my $update (@posts) {
  106. unlink "$datastore/$update->{id}" or confess;
  107. Trog::SQLite::TagIndex::remove_post($update);
  108. }
  109. return 0;
  110. }
  111. sub tags($self) {
  112. return Trog::SQLite::TagIndex::tags();
  113. }
  114. 1;