FlatFile.pm 4.3 KB

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