migrate.pl 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/usr/bin/env perl
  2. #Migrate tCMS1 data to tCMS2 flat file data model
  3. use v5.36;
  4. use re '/aa';
  5. use JSON::MaybeXS;
  6. use File::Slurper();
  7. use HTML::Parser;
  8. use UUID::Tiny ':std';
  9. use File::Copy;
  10. use DateTime;
  11. =head1 SYNOPSIS
  12. Migrate tCMS1 data to the tCMS2 flat file data model.
  13. Walks a tCMS1 docroot and writes each microblog entry and blog post out as a
  14. tCMS2 post under data/files, keyed by the original creation time so that the
  15. ordering survives.
  16. =head2 USAGE
  17. Edit $docroot and $dir below to point at the tCMS1 site, then:
  18. bin/migrate.pl
  19. Run from the tCMS root; the output paths are relative to it. Follow up with
  20. bin/build_index.pl, as this writes the flat files directly and leaves the post
  21. index untouched.
  22. =head2 CAVEATS
  23. Historical. This is the first of the migrate scripts and only applies to a
  24. tCMS1 site; anything newer wants migrate2.pl onwards.
  25. Microblog entries come in two flavours -- JSON, and hand written HTML from
  26. before that -- so each file is tried as JSON first and parsed as HTML if that
  27. fails. Post authorship is recovered from file ownership, with www-data and
  28. nologin owners becoming 'nobody'.
  29. There is an C<exit 0> partway through, before the video migration. That code
  30. is unreachable as written and has never been run against anything current.
  31. =cut
  32. #Edit this to be whatever you need it to be
  33. my $docroot = "/var/www/teodesian.net/doc";
  34. my $dir = "/var/www/teodesian.net/doc/microblog/";
  35. opendir( my $dh, $dir ) or die;
  36. my @days = grep { !/^\./ } readdir $dh;
  37. closedir $dh;
  38. my $ring = JSON::MaybeXS->new();
  39. foreach my $day (@days) {
  40. opendir( my $dht, "$dir/$day" ) or die;
  41. my @times = grep { !/^\./ } readdir $dht;
  42. closedir $dht;
  43. # Escaped on purpose: split compiles a string argument as a regex, so a
  44. # bare '.' would match any character and hand back an empty list.
  45. my ( $month, $date, $year ) = split( '\.', $day );
  46. foreach my $time (@times) {
  47. my ( $hour, $min, $sec ) = split( ':', $time );
  48. my $data;
  49. my $file = "$dir/$day/$time";
  50. print "Migrate $file\n";
  51. eval {
  52. my $content = File::Slurper::read_text($file);
  53. $data = $ring->decode($content);
  54. $data = json_remap($data);
  55. };
  56. $data = html_post($file) unless $data;
  57. my $dt = DateTime->new(
  58. year => $year + 2000,
  59. month => $month,
  60. day => $date,
  61. hour => $hour,
  62. minute => $min,
  63. second => $sec,
  64. );
  65. $data->{created} = $dt->epoch();
  66. $data->{id} = $data->{created};
  67. $data->{tags} = [ 'public', 'news' ];
  68. $data->{version} = 0;
  69. open( my $fh, '>', "data/files/$data->{created}" ) or die;
  70. print $fh encode_json( [$data] );
  71. close $fh;
  72. }
  73. }
  74. # Migrate blog posts
  75. $dir = "$docroot/blog";
  76. opendir( my $bh, $dir ) or die;
  77. my @blogs = grep { -f "$dir/$_" } readdir $bh; ## no critic (ProhibitFiletest_f) -- picking which entries to read
  78. closedir $bh;
  79. my $offset = 0;
  80. foreach my $post (
  81. sort {
  82. my $anum = $a =~ m/^(\d*)-/;
  83. my $bnum = $b =~ m/^(\d*)-/;
  84. $b <=> $a
  85. } @blogs
  86. ) {
  87. my $postname = $post;
  88. $postname =~ s/^\d*-//g;
  89. $postname =~ s/\.post$//g;
  90. my $content = File::Slurper::read_text("$dir/$post");
  91. my $data = {
  92. title => $postname,
  93. data => $content,
  94. tags => [ 'blog', 'public' ],
  95. };
  96. my ( undef, undef, undef, undef, $uid, undef, undef, undef, undef, $ctime ) = stat("$dir/$post");
  97. my $user = lc( getpwuid($uid) );
  98. $user = scalar( grep { $user eq $_ } qw{/sbin/nologin www-data} ) ? 'nobody' : $user;
  99. $data->{user} = $user;
  100. $ctime += $offset;
  101. $data->{created} = $ctime;
  102. $data->{id} = $ctime;
  103. $data->{href} = "/blog/$ctime";
  104. $data->{version} = 0;
  105. print "Migrate blog post '$post'\n";
  106. open( my $fh, '>', "data/files/$data->{created}" ) or die;
  107. print $fh encode_json( [$data] );
  108. close $fh;
  109. $offset--;
  110. }
  111. exit 0;
  112. my $vdir = "$docroot/fileshare/video";
  113. opendir( my $vh, $vdir ) or die;
  114. my @vidyas = grep { -f "$vdir/$_" && $_ =~ m/\.m4v$/ } readdir $vh; ## no critic (ProhibitFiletest_f) -- picking which entries to read
  115. closedir $vh;
  116. foreach my $vid (@vidyas) {
  117. my $postname = $vid;
  118. $postname =~ s/_/ /g;
  119. $postname =~ s/\.mv4$//g;
  120. my $data = {
  121. title => $postname,
  122. data => "Description forthcoming",
  123. tags => [ 'video', 'public' ],
  124. preview => "/img/sys/testpattern.jpg",
  125. };
  126. my ( undef, undef, undef, undef, $uid, undef, undef, undef, undef, $ctime ) = stat("$vdir/$vid");
  127. my $user = lc( getpwuid($uid) );
  128. $user = scalar( grep { $user eq $_ } qw{/sbin/nologin www-data} ) ? 'nobody' : $user;
  129. $data->{user} = $user;
  130. $data->{created} = $ctime;
  131. $data->{id} = $ctime;
  132. $data->{href} = "/assets/$ctime-$vid";
  133. $data->{version} = 0;
  134. #Copy over the video
  135. File::Copy::copy( "$vdir/$vid", "www/assets/$ctime-$vid" );
  136. print "Migrate video '$vid'\n";
  137. open( my $fh, '>', "data/files/$data->{created}" ) or die;
  138. print $fh encode_json( [$data] );
  139. close $fh;
  140. }
  141. sub json_remap {
  142. my $json = shift;
  143. return {
  144. preview => $json->{"image"},
  145. data => $json->{"comment"},
  146. user => lc( $json->{"poster"} ),
  147. title => $json->{"title"},
  148. audio_href => $json->{"audio"},
  149. href => $json->{"url"},
  150. video_href => $json->{"video"},
  151. };
  152. }
  153. sub html_post {
  154. my $file = shift;
  155. my $is_first_link = 1;
  156. my $data = { data => '', href => '' };
  157. my $p = HTML::Parser->new(
  158. handlers => [
  159. start => [
  160. sub {
  161. my ( $self, $attr, $text, $tagname ) = @_;
  162. if ( $tagname eq 'a' && $is_first_link ) {
  163. $data->{href} = $attr->{href};
  164. return;
  165. }
  166. return if $is_first_link;
  167. return if $tagname eq 'hr';
  168. $data->{data} .= $text;
  169. },
  170. 'self, attr, text,tagname'
  171. ],
  172. text => [
  173. sub {
  174. my ( $self, $attr, $text, $tagname ) = @_;
  175. if ($is_first_link) {
  176. $data->{title} .= $text;
  177. return;
  178. }
  179. $data->{data} .= $text;
  180. },
  181. 'self, attr, text,tagname'
  182. ],
  183. end => [
  184. sub {
  185. my ( $self, $attr, $text, $tagname ) = @_;
  186. if ( $tagname eq 'a' && $is_first_link ) {
  187. $is_first_link = 0;
  188. return;
  189. }
  190. return if $is_first_link;
  191. $data->{data} .= $text;
  192. },
  193. 'self, attr, text,tagname'
  194. ],
  195. ],
  196. );
  197. $p->parse_file($file);
  198. #Get the user name from ownership
  199. my ( undef, undef, undef, undef, $uid, undef, undef, undef, undef, $ctime ) = stat($file);
  200. my $user = lc( getpwuid($uid) );
  201. $user = scalar( grep { $user eq $_ } qw{/sbin/nologin www-data} ) ? 'nobody' : $user;
  202. $data->{user} = $user;
  203. $data->{created} = $ctime;
  204. return $data;
  205. }