FlatFile.pm 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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 Path::Tiny();
  12. use lib 'lib';
  13. use Trog::SQLite::TagIndex;
  14. use parent qw{Trog::DataModule};
  15. our $datastore = 'data/files';
  16. sub lang { 'Perl Regex in Quotemeta' }
  17. sub help { 'https://perldoc.perl.org/functions/quotemeta.html' }
  18. =head1 Trog::Data::FlatFile
  19. This data model has multiple drawbacks, but is "good enough" for most low-content and few editor applications.
  20. You can only post once per second due to it storing each post as a file named after the timestamp.
  21. =cut
  22. our $parser = JSON::MaybeXS->new( utf8 => 1);
  23. # Initialize the list of posts by tag for all known tags.
  24. # This is because the list won't ever change between HUPs
  25. our @tags = Trog::SQLite::TagIndex::tags();
  26. our %posts_by_tag;
  27. sub read ($self, $query={}) {
  28. $query->{limit} //= 25;
  29. #Optimize direct ID
  30. my @index;
  31. if ($query->{id}) {
  32. @index = ("$datastore/$query->{id}");
  33. } else {
  34. # Remove tags which we don't care about and sort to keep memoized memory usage down
  35. @{$query->{tags}} = sort grep { my $t = $_; grep { $t eq $_ } @tags } @{$query->{tags}};
  36. my $tagkey = join('&',@{$query->{tags}});
  37. # Check against memoizer
  38. $posts_by_tag{$tagkey} //= [];
  39. @index = @{$posts_by_tag{$tagkey}} if @{$posts_by_tag{$tagkey}};
  40. if (!@index && -f 'data/posts.db') {
  41. @index = map { "$datastore/$_" } Trog::SQLite::TagIndex::posts_for_tags(@{$query->{tags}});
  42. $posts_by_tag{$tagkey} = \@index;
  43. }
  44. @index = $self->_index() unless @index;
  45. }
  46. my @items;
  47. foreach my $item (@index) {
  48. next unless -f $item;
  49. my $slurped = eval { File::Slurper::read_text($item) };
  50. if (!$slurped) {
  51. print "Failed to Read $item:\n$@\n";
  52. next;
  53. }
  54. my $parsed = eval { $parser->decode($slurped) };
  55. if (!$parsed) {
  56. # Try and read it in binary in case it was encoded incorrectly the first time
  57. $slurped = eval { File::Slurper::read_binary($item) };
  58. $parsed = eval { $parser->decode($slurped) };
  59. if (!$parsed) {
  60. print "JSON Decode error on $item:\n$@\n";
  61. next;
  62. }
  63. }
  64. #XXX this imposes an inefficiency in itself, get() will filter uselessly again here
  65. my @filtered = $query->{raw} ? @$parsed : $self->filter($query,@$parsed);
  66. push(@items,@filtered) if @filtered;
  67. next if $query->{limit} == 0; # 0 = unlimited
  68. last if scalar(@items) == $query->{limit};
  69. }
  70. return \@items;
  71. }
  72. sub _index ($self) {
  73. confess "Can't find datastore in $datastore !" unless -d $datastore;
  74. opendir(my $dh, $datastore) or confess;
  75. my @index = grep { -f } map { "$datastore/$_" } readdir $dh;
  76. closedir $dh;
  77. return sort { $b cmp $a } @index;
  78. }
  79. sub routes ($self) {
  80. return Trog::SQLite::TagIndex::routes();
  81. }
  82. sub aliases ($self) {
  83. return Trog::SQLite::TagIndex::aliases();
  84. }
  85. sub write($self,$data) {
  86. foreach my $post (@$data) {
  87. my $file = "$datastore/$post->{id}";
  88. my $update = [$post];
  89. if (-f $file) {
  90. my $slurped = File::Slurper::read_binary($file);
  91. my $parsed = $parser->decode($slurped);
  92. $update = [(@$parsed, $post)];
  93. }
  94. mkdir $datastore;
  95. open(my $fh, '>', $file) or confess "Could not open $file";
  96. print $fh $parser->encode($update);
  97. close $fh;
  98. Trog::SQLite::TagIndex::add_post($post,$self);
  99. }
  100. }
  101. sub count ($self) {
  102. my @index = $self->_index();
  103. return scalar(@index);
  104. }
  105. sub delete($self, @posts) {
  106. foreach my $update (@posts) {
  107. unlink "$datastore/$update->{id}" or confess;
  108. Trog::SQLite::TagIndex::remove_post($update);
  109. }
  110. # Gorilla cache invalidation
  111. Path::Tiny::path('www/statics')->remove_tree;
  112. return 0;
  113. }
  114. sub tags($self) {
  115. return Trog::SQLite::TagIndex::tags();
  116. }
  117. 1;