FlatFile.pm 4.3 KB

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