<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Cloud Platform Topics</title>
    <link>https://cloud.writeas.com/</link>
    <description>Various sysadmin, app development, and cloud hosting topics</description>
    <pubDate>Sun, 05 Apr 2026 17:43:37 +0000</pubDate>
    <item>
      <title>Adding search to a write.as public blog </title>
      <link>https://cloud.writeas.com/adding-search-to-a-write-as-public-blog?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Updated info after this post was created: CJ Eller&#39;s blog post explains how to custom style the search bar with javascript and CSS.  He also provides further details on this in two posts on the write.as community forum: post 1 and post2.&#xA;hr style=&#34;margin-top:6px; margin-bottom:0px;&#34;&#xA;&#xA;Searching a public write.as or writefreely blog is easy to do, compared to searching private blogs or anonymous posts. It just takes adding an html iframe snippet to a blog page&#39;s source code, no javascript or CSS is needed. The blog must be public, if it&#39;s a private blog it won&#39;t work. For an example, take a look at this blog page: https://plaintextproject.online/articles.html. Then view the source code for that page (typically, right-click on the browser page and select View Source).  You&#39;ll find the following snippet embedded in that code: !--more--&#xA;&#xA;iframe src=&#34;https://duckduckgo.com/search.html?width=250&amp;site=plaintextproject.online&amp;prefill=Search The Plain Text Project&#34; style=&#34;overflow:hidden;margin:0;padding:0;width:308px;height:40px;border-radius: 15px 50px 30px 5px;&#34; frameborder=&#34;0&#34;/iframe&#xA;&#xA;The DuckDuckGo search engine is used. Replace two parts of that code:&#xA;&#xA;Replace plaintextproject.online with either your personal domain name or your blog.writeas.com blog.  &#xA;&#xA;Replace Search The Plain Text Project with your custom search message.&#xA;&#xA;A couple points:&#xA;&#xA;You can search other public blogs or web sites if you want. It doesn&#39;t have to be your own blog.&#xA;&#xA;Since this snippet is html with no javascript or CSS, this will work in an write.as/writefreely anonymous post also. However, you would have to search a public blog or site. Anonymous posts and private write.as blogs are not searchable with this technique (though see point #4 for a different way to search them).&#xA;&#xA;If your blog&#39;s url is in the form write.as/blogname rather than blogname.writeas.com, that might work as well. I quickly did a test and it appeared to work.&#xA;&#xA;If your write.as/writefreely blog is private or if you want to search your anonymous posts, you can still do it using, for example, a PHP program in combination with data downloaded from the write.as API. This technique is outlined here.  The technique described in that post requires that you have a web server with PHP support.  If you don&#39;t have that, CJ Eller,  community manager for write.as, wrote a glitch.com app that will do the search on your blog(s) and anonymous data downloaded via the write.as and/or writefreely API. &#xA;&#xA;Of course, this html iframe snippet will also work on other web site pages besides just those created on write.as or writefreely.&#xA;&#xA;Thanks Plain Text Project for giving an example of how this can be done.]]&gt;</description>
      <content:encoded><![CDATA[<p>Updated info after this post was created: CJ Eller&#39;s blog post explains <a href="https://blog.cjeller.site/new-year-new-search" rel="nofollow">how to custom style the search bar with javascript and CSS</a>.  He also provides further details on this in two posts on the write.as community forum: <a href="https://discuss.write.as/t/search-bar/1088/8" rel="nofollow">post 1</a> and <a href="https://discuss.write.as/t/search-bar/1088/7" rel="nofollow">post2</a>.
<hr style="margin-top:6px; margin-bottom:0px;"></p>

<p>Searching a public write.as or writefreely blog is easy to do, compared to <a href="https://write.as/cloud/example-program-to-search-write-as-writefreely-api-data" rel="nofollow">searching private blogs or anonymous posts</a>. It just takes adding an html <code>iframe</code> snippet to a blog page&#39;s source code, no javascript or CSS is needed. The blog must be public, if it&#39;s a private blog it won&#39;t work. For an example, take a look at this blog page: <a href="https://plaintextproject.online/articles.html" rel="nofollow">https://plaintextproject.online/articles.html</a>. Then view the source code for that page (typically, right-click on the browser page and select View Source).  You&#39;ll find the following snippet embedded in that code: </p>

<p><code>&lt;iframe src=&#34;https://duckduckgo.com/search.html?width=250&amp;site=plaintextproject.online&amp;prefill=Search The Plain Text Project&#34; style=&#34;overflow:hidden;margin:0;padding:0;width:308px;height:40px;border-radius: 15px 50px 30px 5px;&#34; frameborder=&#34;0&#34;&gt;&lt;/iframe&gt;</code></p>

<p>The DuckDuckGo search engine is used. Replace two parts of that code:</p>
<ol><li><p>Replace <code>plaintextproject.online</code> with either your personal domain name or your <code>&lt;blog&gt;.writeas.com</code> blog.</p></li>

<li><p>Replace <code>Search The Plain Text Project</code> with your custom search message.</p></li></ol>

<p>A couple points:</p>
<ol><li><p>You can search other public blogs or web sites if you want. It doesn&#39;t have to be your own blog.</p></li>

<li><p>Since this snippet is html with no javascript or CSS, this will work in an write.as/writefreely anonymous post also. However, you would have to search a public blog or site. Anonymous posts and private write.as blogs are not searchable with this technique (though see point #4 for a different way to search them).</p></li>

<li><p>If your blog&#39;s url is in the form <code>write.as/blogname</code> rather than <code>blogname.writeas.com</code>, that <em>might</em> work as well. I quickly did a test and it appeared to work.</p></li>

<li><p>If your write.as/writefreely blog is private or if you want to search your anonymous posts, you can still do it using, for example, a PHP program in combination with data downloaded from the <a href="https://developers.write.as/docs/api/" rel="nofollow">write.as API</a>. This technique is outlined <a href="https://write.as/cloud/example-program-to-search-write-as-writefreely-api-data" rel="nofollow">here</a>.  The technique described in that post requires that you have a web server with PHP support.  If you don&#39;t have that, <a href="https://blog.cjeller.site/" rel="nofollow">CJ Eller</a>,  community manager for write.as, wrote a <a href="https://glitch.com/~brave-grenadilla" rel="nofollow">glitch.com app that will do the search</a> on your blog(s) and anonymous data downloaded via the write.as and/or writefreely API.</p></li>

<li><p>Of course, this html iframe snippet will also work on other web site pages besides just those created on write.as or writefreely.</p></li></ol>

<p>Thanks <a href="https://plaintextproject.online/articles.html" rel="nofollow">Plain Text Project</a> for giving an example of how this can be done.</p>
]]></content:encoded>
      <guid>https://cloud.writeas.com/adding-search-to-a-write-as-public-blog</guid>
      <pubDate>Sun, 02 Feb 2020 15:47:28 +0000</pubDate>
    </item>
    <item>
      <title>Example program to search Write.as/WriteFreely posts from API JSON data</title>
      <link>https://cloud.writeas.com/example-program-to-search-write-as-writefreely-api-data?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[A technique exists for searching public write.as or writefreely blogs that is documented in this post. However, if you want to search your anonymous posts and/or private blogs, the following technique using the API works well for that purpose. &#xA;&#xA;Recently I began to look into the API features of  Write.as and WriteFreely, specifically to use the API to get the content of all my blog posts and anonymous posts in JSON format for the purpose of doing compound searches on this data.&#xA;&#xA;My intention was to periodically download the data to my website using the API and to code one or a series of custom PHP programs to query it from a web page.&#xA; &#xA;The code below is an example showing the API command along with a simple program to do a compound search of the data. !--more--&#xA;&#xA;The PHP program illustrates navigating the JSON data and can be used to query one or multiple user instances. The data from a user&#39;s instances may include multiple blogs (whether private or published) and any number of anonymous posts.&#xA;&#xA;The program output lists only those posts that have a match to your input keywords. The post title is a hyperlink that takes you directly to the post. There is also a link to the individual blog or anonymous posts page.  &#xA;&#xA;This code is freely available, as is, to anyone who may wish to use or modify it, but no support is provided for it. I am not an expert-level PHP programmer and this was a quick coding job, so I don&#39;t consider it to be particularly elegant coding. But hey, it works. I have this installed on my web server (Debian 10, Apache 2.4, and PHP 7.3). It could be installed on your local machine as well, and I&#39;ve tested it in Windows 10 with XAMPP. It probably would also work on MacOS provided you have a web server with PHP.&#xA;&#xA;The JSON files generated by the API need to be downloaded for each user Write.as or WriteFreely instance before running this program. Here&#39;s a curl command example for getting all user posts via the API:&#xA;curl &#34;https://write.as/api/me/posts&#34; \&#xA;   -H &#34;Authorization: Token 00000000-1111-2222-3333-444444444444&#34; \&#xA;   -H &#34;Content-Type: application/json&#34; \&#xA;   -X GET    writeasapiposts.json&#xA;If you don&#39;t yet have an authorization token, see this to get one:  https://developers.write.as/docs/api/#authenticate-a-user&#xA;&#xA;The beginning of the PHP program has a Required Input section where you need to specify your data files, the urls to your Write.as and/or WriteFreely instance(s)sup1/sup, and a flag involving markdown output.  See the code for more info.  You don&#39;t need to specify your individual blogs. Based on your auth code, the API will take care of downloading all your blogs and anonymous posts.  The urls are used only to construct the hyperlink to the matched posts - they are not used to retrieve data, the data comes only from the API.&#xA;&#xA;sup1/sup As an example, I included two WriteFreely services, https://qua.name and https://wordsmith.social.  Note that if you have custom domain names for your instances, you would of course use those.&#xA;&#xA;Here&#39;s an image clip of an input form and output  example:&#xA;&#xA;wawf-search-clip&#xA;&#xA;Here is the PHP code. For display convenience this includes all the HTML, CSS, and PHP code in one file.  A more complex example would use separate files.  &#xA;&#xA;Note: If you have a Write.as and/or WriteFreely blog and want want to try this out but don&#39;t have access to a server running PHP, it can be run on glitch.com. Instructions are provided here by CJ Eller, who has packaged this into a convenient, easy-to-use glitch app. General glitch.com Help topics are here. For info regarding public vs private visibility of the JSON data files in the glitch app, see this post.&#xA;&#xA;&lt;?php&#xA;iniset(&#39;displayerrors&#39;, 1);&#xA;iniset(&#39;displaystartuperrors&#39;, 1);&#xA;errorreporting(EALL);&#xA;&#xA;/======================================================================================&#xA;  REQUIRED INPUT:&#xA;&#xA;  Specify the filenames for your posts API data, your write.as and/or writefreely blogs, &#xA;    and for the $mdext value enter either &#39;&#39; or &#39;.md&#39; depending on what&#39;s needed for the &#xA;    anonymous post files to be rendered in markdown (write.as anon posts typically need &#xA;    the &#39;.md&#39; whereas writefreely anon posts typically do not).&#xA;  Modify this associative array for your situation. Include a trailing forward slash, /,&#xA;    in the $href element. Add a folder path prefix to the $fn filename if needed.&#xA;  The API data files should be in the same folder as this program.  &#xA;/&#xA;$blog[0] = array(&#34;fn&#34; =  &#34;wa-posts.json&#34;, &#34;href&#34; =  &#34;https://write.as/&#34;, &#34;mdext&#34; =  &#39;.md&#39;);&#xA;$blog[1] = array(&#34;fn&#34; =  &#34;qua-posts.json&#34;, &#34;href&#34; =  &#34;https://my.writefreely.instance/&#34;, &#34;mdext&#34; =  &#39;&#39;);&#xA;$blog[2] = array(&#34;fn&#34; =  &#34;wordsmith-posts.json&#34;, &#34;href&#34; =  &#34;https://wordsmith.social/&#34;, &#34;mdext&#34; =  &#39;&#39;);&#xA;/======================================================================================/&#xA;    &#xA;/======================================================================================&#xA;  The API data JSON files need to be downloaded before running this program. Here&#39;s a &#xA;  curl command example for getting all user posts via the write.as or writefreely API:&#xA;    &#xA;  curl &#34;https://write.as/api/me/posts&#34; \&#xA;    -H &#34;Authorization: Token 00000000-1111-2222-3333-444444444444&#34; \&#xA;    -H &#34;Content-Type: application/json&#34; \&#xA;    -X GET    writeasapiposts.json&#xA;    &#xA;  If you don&#39;t yet have an auth token, see this to get one:&#xA;  https://developers.write.as/docs/api/#authenticate-a-user&#xA;/&#xA;&#xA;// IMPORTANT NOTES:&#xA;//&#xA;// For anonymous posts, in order for the Title field to be populated in the API data,&#xA;// the first line of the post MUST start with a single # and a space. Otherwise, the &#xA;// Title field will be blank.&#xA;//&#xA;// For the names of the elements provided by the API data, see bottom of this file&#xA;?  !DOCTYPE HTML&#xA;html&#xA;head&#xA;  meta http-equiv=&#34;content-type&#34; content=&#34;text/html; charset=UTF-8&#34;&#xA;  titleWaWf API Data Search/title&#xA;  style type=&#34;text/css&#34;&#xA;    body {&#xA;      font-family: sans-serif, helvetica;&#xA;      font-size: 15px;&#xA;      line-height: 1.3;&#xA;      margin: 15px;&#xA;    }&#xA;    a {&#xA;       color: blue;&#xA;       text-decoration: none;&#xA;     }&#xA;     a:hover {&#xA;       color: #AA08AA; &#xA;       font-weight: bold;&#xA;       text-decoration: underline;&#xA;     }&#xA;  /style&#xA;/head&#xA;body &#xA;&lt;?php&#xA;//==========================================&#xA;// Main part of program&#xA;//==========================================&#xA;$msg = &#39;Search these files: &#39;;&#xA;$i = 0;&#xA;foreach($blog as $item) {&#xA;  $fn = $item[&#39;fn&#39;];&#xA;  if (!fileexists($fn)) {&#xA;    echo &#34;span style=\&#34;color:red;\&#34;bError: File $fn not found; please check and either correct filename or remove it from the list (i.e., the relevant &#34; . &#39;$blog[] entry in this program)/b/span&#39;;&#xA;    exit;&#xA;  } else {&#xA;  $msg .= $fn . &#34;, &#34;;&#xA;  }&#xA;  $href = $item[&#39;href&#39;];&#xA;  if (substr($href, -1) !== &#39;/&#39;) $blog$i = $href . &#39;/&#39;;&#xA;}  &#xA;echo substr($msg, 0, -2) . &#34;brbr&#34;;&#xA;$search1=&#39;&#39;; $search2=&#39;&#39;; $search3=&#39;&#39;; $butnot=&#39;&#39;; $bOption=&#39;&#39;; $nq=&#39;&#39;;&#xA;&#xA;if (isset($POST[&#39;submit&#39;])) {&#xA;  $search1 = $POST[&#39;searchkey1&#39;];&#xA;  $search2 = $POST[&#39;searchkey2&#39;];&#xA;  $search3 = $POST[&#39;searchkey3&#39;];&#xA;  $butnot =  $POST[&#39;butnot&#39;];&#xA;  $bOption = $POST[&#39;bOption&#39;];&#xA;  $uri = $SERVER[&#39;REQUESTURI&#39;];&#xA;  $nq = &#39;a href=&#34;&#39; . $uri . &#39;&#34;New Query/abr&#39;;&#xA;}&#xA;&#xA;print &#39;form name=&#34;myform&#34; action=&#34;&#39; . $SERVER[&#39;PHPSELF&#39;] . &#39;&#34; method=&#34;post&#34;&#xA;&#x9;Keyword1: (&amp;nbsp;input name=&#34;searchkey1&#34; size=&#34;15&#34; autofocus value=&#34;&#39; . $search1 . &#39;&#34;&amp;nbsp;bAND/b&#xA;&#x9;Keyword2: input name=&#34;searchkey2&#34; size=&#34;15&#34; value=&#34;&#39; . $search2 . &#39;&#34;&amp;nbsp;)&amp;nbsp;&#xA; &#x9;select name=&#34;bOption&#34;&#39;;&#xA;if ($bOption == &#39;OR&#39;) {&#xA;  print&#x9;&#39;option value=&#34;AND&#34;AND/optionoption value=&#34;OR&#34; selectedOR/option&#39;;&#xA;} else {&#xA;  print&#x9;&#39;option value=&#34;AND&#34; selectedAND/optionoption value=&#34;OR&#34;OR/option&#39;;&#xA;}  &#xA;print&#x9;&#39;/select &#xA;&#x9;Keyword3: input name=&#34;searchkey3&#34; size=&#34;15&#34; value=&#34;&#39; . $search3 . &#39;&#34;&amp;nbsp;&#xA;&#x9;But Not: input name=&#34;butnot&#34; size=&#34;15&#34; value=&#34;&#39; . $butnot . &#39;&#34;&amp;nbsp;&#xA;&#x9;input type=&#34;submit&#34; name=&#34;submit&#34; value=&#34;Go&#34;&amp;nbsp;&amp;nbsp;&#39; . $nq . &#39;/form&#39;;&#xA;if (!isset($POST[&#39;submit&#39;])) exit;&#xA;&#xA;// The &#34;submit&#34; variable exists, so the form has been submitted - process form input and display results&#xA;&#xA;// The API data file can have multiple blogs along with anonymous posts and they may be all mixed together with no &#xA;// separation and no sort order. Therefore, if you want to have all anon posts and each blog&#39;s posts together in grouped&#xA;// sections, a sorting mechanism is needed. We sort on the url (which groups each blog&#39;s posts together&#39;) and to handle &#xA;// the anon posts, we add a class=&#34;anon&#34; to the url so they&#39;ll be grouped together when sorted.&#xA;&#xA;if ($search1 == &#34;&#34; &amp;&amp; $search2 == &#34;&#34;) {&#xA;  echo &#39;brbspan style=&#34;color:red;&#34;You must input at least one search keyword in the first 2 input boxes/span/b&#39;;&#xA;  exit;&#xA;}&#xA;&#xA;$iMatches = 0;&#xA;foreach($blog as $item) {&#xA;  $fn = $item[&#39;fn&#39;];&#xA;  $href = $item[&#39;href&#39;];&#xA;  $mdext = $item[&#39;mdext&#39;];&#xA;  $json = filegetcontents($fn);&#xA;  $array = jsondecode($json, true);&#xA;  $blogposts = $array[&#39;data&#39;];&#xA;  $lines=&#39;&#39;;&#xA;  &#xA;  foreach ($blogposts as $post) {  &#xA;    $id    = $post[&#39;id&#39;];&#xA;    $slug  = $post[&#39;slug&#39;];&#xA;    $title = $post[&#39;title&#39;];&#xA;    // title might be blank; if so, substitute the slug if it&#39;s not also blank, otherwise use the post id&#xA;    if (trim($title) == &#39;&#39;) {&#xA;      if (trim($slug) !== &#39;&#39;) {&#xA;        $title = $slug;&#xA;      } else {&#xA;        $title = $id;&#xA;      }&#xA;    }&#xA;    $tags  = $post[&#39;tags&#39;];  // note $tags is an array&#xA;    $body  = $post[&#39;body&#39;];&#xA;    $url = &#39;a href=&#34;&#39; . $href . $id . &#39;.md&#34; target=&#34;blank&#34;&#39; . &#34;b$title | $slug/b/abr\n&#34;; &#xA;    //Check title, tags, and body of blog post for keyword matches - entire post vs individual lines?&#xA;    $contents = $title . &#39;||&#39; . implode(&#34; &#34;, $tags) . &#39;||&#39; . $body; //use || separator so don&#39;t have false match possibility due to a run-on &#xA;    if (isMatched($contents)) {&#xA;      // Matched&#xA;      if (isset($post[&#39;collection&#39;])) {&#xA;        // Blog post&#xA;        $coll = $post[&#39;collection&#39;];&#xA;        $cAlias = $coll[&#39;alias&#39;];&#xA;        $blogUrl = &#39;a href=&#34;&#39; . $href .  $coll[&#39;alias&#39;] . &#39;&#34; target=&#34;blank&#34;&#39; . $cAlias . &#34;/a&#34;; &#xA;        $url = &#39;a href=&#34;&#39; . $href . $cAlias . &#39;/&#39; . $slug . &#39;&#34; target=&#34;blank&#34;&#39; . &#34;b$title/b/a&#34;; &#xA;        $lines .=  substr($cAlias,0,3) . &#34;$blogUrl | $urlbr\n&#34;;&#xA;      } else {&#xA;        // Anonymous posts&#xA;        // They typically have a null slug (possible exception is if they&#39;ve been moved from blog post to anonymous post)&#xA;        $url = &#39;a href=&#34;&#39; . $href . $id . $mdext . &#39;&#34; target=&#34;blank&#34;&#39; . &#34;b$title/b/abr\n&#34;;&#xA;        $lines .= &#39;ano&#39; . &#39;a class=&#34;anon&#34; href=&#34;&#39; . $href . &#39;me/posts&#34;Anonymous/a&#39; . &#34; | $urlbr\n&#34;;   //class=&#34;anon&#34; needed for sort&#xA;      }&#xA;      $iMatches += 1;&#xA;    }  &#xA;  } // end foreach blog post&#xA;  &#xA;  if ($iMatches == 0) {&#xA;    echo &#34;brNo matches were found for the search criteria&#34;;&#xA;    exit;&#xA;  }&#xA;  &#xA;  // $lines contains each blog post. Sort them so all anon posts and separate blog posts group together.&#xA;  $posts = explode(&#34;\n&#34;, $lines);&#xA;  sort($posts, SORTNATURAL | SORTFLAGCASE);&#xA;  $prev = &#39;&#39;;&#xA;  $i = 1;&#xA;  $itot = 0;&#xA;  foreach($posts as $p) {&#xA;    if(strlen($p)   4) {&#xA;      if (substr($p,0,3) !== $prev) {&#xA;        echo &#34;br\n&#34;;&#xA;        $i = 1;&#xA;      }  &#xA;      echo strpad($i, 2, &#39;0&#39;, STRPADLEFT) . &#34;: &#34; . substr($p,3) . &#34;\n&#34;;&#xA;      $i++;&#xA;      $itot += 1;&#xA;      $prev = substr($p, 0,3);&#xA;    }&#xA;  }&#xA;} // end foreach blog&#xA;&#xA;echo &#34;br$iMatches matches were found for the search criteria&#34;;&#xA;&#xA;function isMatched($contents) {&#xA;  global $search1;&#xA;  global $search2;&#xA;  global $search3;&#xA;  global $butnot;&#xA;  global $bOption;  //applies solely to the 3rd searchkey input&#xA;&#xA;  if (stripos($contents, $butnot) !== false) return false;  &#xA;  if ($bOption == &#39;OR&#39; &amp;&amp; $search3   &#39;&#39; &amp;&amp; stripos($contents, $search3) !== false) return true;&#xA; &#xA;  // The ButNot and OR criteria were handled above, so now we only need to check search &#xA;  // term 1 by itself if it&#39;s the only input or in cobmination with any term 2 and 3 input&#xA;   if ($search1 == &#34;&#34; &amp;&amp; $search2 !== &#34;&#34;) {&#xA;  &#x9;$search1 = $search2;&#xA;  &#x9;$search2 = &#34;&#34;;&#xA;  }&#x9;&#xA;   if ($search2 == &#34;&#34; &amp;&amp; $search3 !== &#34;&#34; &amp;&amp; $bOption == &#34;AND&#34;) {&#xA;  &#x9;$search2 = $search3;&#xA;  &#x9;$search3 = &#34;&#34;;&#xA;  }&#x9;&#xA;  // By the program logic, search1 is guaranteed to be be populated&#xA;  // So if there is no search1 match and since only AND logic now applies here, return false&#xA;  if (stripos($contents,$search1) === false) return false;&#xA;  // We now know that there is a $search1 match&#xA;  if ($search2 == &#39;&#39; &amp;&amp; $search3 == &#39;&#39;) return true;&#xA;  if ($search2   &#39;&#39; &amp;&amp; stripos($contents, $search2) !== false  &amp;&amp; $search3 == &#39;&#39;) return true;&#xA;  if ($search2   &#39;&#39; &amp;&amp; stripos($contents, $search2) !== false  &amp;&amp; $search3   &#39;&#39; &amp;&amp; stripos($contents, $search3) !== false) return true;&#xA;  return false;&#xA;}&#xA;&#xA;/&#xA;Example elements in the JSON API data&#xA;  &#34;id&#34;: &#34;xxxxxxxxxx&#34;,&#xA;  &#34;slug&#34;: &#34;confidence-level-contours&#34;,&#xA;  &#34;appearance&#34;: &#34;sans&#34;,&#xA;  &#34;language&#34;: &#34;en&#34;,&#xA;  &#34;rtl&#34;: false,&#xA;  &#34;created&#34;: &#34;2019-11-03T15:46:58Z&#34;,&#xA;  &#34;updated&#34;: &#34;2019-11-03T16:09:50Z&#34;,&#xA;  &#34;title&#34;: &#34;Confidence level contours&#34;,&#xA;  &#34;body&#34;: &#34;Some body text&#34;,&#xA;  &#34;tags&#34;: [],&#xA;  &#34;images&#34;: [&#xA;     &#34;https://i.snap.as/xxxxxxx.jpg&#34;,&#xA;     &#34;https://i.snap.as/yyyyyyy.jpg&#34;,&#xA;   ],&#xA;  &#34;views&#34;: 0,&#xA;  &#34;collection&#34;: {&#xA;     &#34;alias&#34;: &#34;MyBlogName&#34;,&#xA;     &#34;title&#34;: &#34;My Blog Title&#34;,&#xA;     &#34;description&#34;: &#34;Primarily About this, that, and the other&#34;,&#xA;     &#34;stylesheet&#34;: &#34;&#34;,&#xA;     &#34;public&#34;: false,&#xA;     &#34;views&#34;: 22,&#xA;     &#34;total_posts&#34;: 0&#xA;     }&#xA;/&#xA;&#xA;?  /body&#xA;/html&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p>A technique exists for searching <em><strong>public</strong></em> write.as or writefreely blogs that is documented in <a href="https://write.as/cloud/adding-search-to-a-write-as-public-blog" rel="nofollow">this post</a>. However, if you want to search your anonymous posts and/or private blogs, the following technique using the API works well for that purpose.</p>

<p>Recently I began to look into the <a href="https://developers.write.as/docs/api/" rel="nofollow">API features</a> of  <a href="https://write.as/about" rel="nofollow">Write.as</a> and <a href="https://writefreely.org" rel="nofollow">WriteFreely</a>, specifically to use the API to get the content of all my blog posts and anonymous posts in <a href="https://www.json.org" rel="nofollow">JSON</a> format for the purpose of doing compound searches on this data.</p>

<p>My intention was to periodically download the data to my website using the API and to code one or a series of custom <a href="https://www.php.net/" rel="nofollow">PHP</a> programs to query it from a web page.</p>

<p>The code below is an example showing the API command along with a simple program to do a compound search of the data. </p>

<p>The PHP program illustrates navigating the JSON data and can be used to query one or multiple user instances. The data from a user&#39;s instances may include multiple blogs (whether private or published) and any number of anonymous posts.</p>

<p>The program output lists only those posts that have a match to your input keywords. The post title is a hyperlink that takes you directly to the post. There is also a link to the individual blog or anonymous posts page.</p>

<p>This code is freely available, as is, to anyone who may wish to use or modify it, but no support is provided for it. I am not an expert-level PHP programmer and this was a quick coding job, so I don&#39;t consider it to be particularly elegant coding. But hey, it works. I have this installed on my web server (Debian 10, Apache 2.4, and PHP 7.3). It could be installed on your local machine as well, and I&#39;ve tested it in Windows 10 with <a href="https://en.wikipedia.org/wiki/XAMPP" rel="nofollow">XAMPP</a>. It probably would also work on MacOS provided you have a web server with PHP.</p>

<p>The JSON files generated by the API need to be downloaded for each user Write.as or WriteFreely instance before running this program. Here&#39;s a <a href="https://www.tecmint.com/linux-curl-command-examples/" rel="nofollow">curl command</a> example for getting all user posts via the API:</p>

<pre><code>curl &#34;https://write.as/api/me/posts&#34; \
   -H &#34;Authorization: Token 00000000-1111-2222-3333-444444444444&#34; \
   -H &#34;Content-Type: application/json&#34; \
   -X GET  &gt;writeas_api_posts.json
</code></pre>

<p>If you don&#39;t yet have an authorization token, see this to get one:  <a href="https://developers.write.as/docs/api/#authenticate-a-user" rel="nofollow">https://developers.write.as/docs/api/#authenticate-a-user</a></p>

<p>The beginning of the PHP program has a Required Input section where you need to specify your data files, the urls to your Write.as and/or WriteFreely instance(s)<sup>1</sup>, and a flag involving <a href="https://www.markdownguide.org/" rel="nofollow">markdown</a> output.  See the code for more info.  You don&#39;t need to specify your individual blogs. Based on your auth code, the API will take care of downloading all your blogs and anonymous posts.  The urls are used only to construct the hyperlink to the matched posts – they are not used to retrieve data, the data comes only from the API.</p>

<p><sup>1</sup> As an example, I included two WriteFreely services, <a href="https://qua.name" rel="nofollow">https://qua.name</a> and <a href="https://wordsmith.social" rel="nofollow">https://wordsmith.social</a>.  Note that if you have custom domain names for your instances, you would of course use those.</p>

<p>Here&#39;s an image clip of an input form and output  example:</p>

<p><img src="https://i.snap.as/egs3F22.jpg" alt="wawf-search-clip"/></p>

<p>Here is the PHP code. For display convenience this includes all the HTML, CSS, and PHP code in one file.  A more complex example would use separate files.</p>

<p>Note: If you have a Write.as and/or WriteFreely blog and want want to try this out but don&#39;t have access to a server running PHP, it can be run on <a href="https://glitch.com/about" rel="nofollow">glitch.com</a>. Instructions are provided <a href="https://discuss.write.as/t/how-to-download-the-export-json-file-for-an-instance-e-g-using-curl-or-programmatically/981/16" rel="nofollow">here</a> by <a href="https://blog.cjeller.site" rel="nofollow">CJ Eller</a>, who has packaged this into a convenient, easy-to-use glitch app. General glitch.com Help topics are <a href="https://glitch.com/help/" rel="nofollow">here</a>. For info regarding public vs private visibility of the JSON data files in the glitch app, see this <a href="https://discuss.write.as/t/how-to-download-the-export-json-file-for-an-instance-e-g-using-curl-or-programmatically/981/17" rel="nofollow">post</a>.</p>

<pre><code>&lt;?php
ini_set(&#39;display_errors&#39;, 1);
ini_set(&#39;display_startup_errors&#39;, 1);
error_reporting(E_ALL);

/*======================================================================================
  REQUIRED INPUT:
========================================================================================
  Specify the filenames for your posts API data, your write.as and/or writefreely blogs, 
    and for the $md_ext value enter either &#39;&#39; or &#39;.md&#39; depending on what&#39;s needed for the 
    anonymous post files to be rendered in markdown (write.as anon posts typically need 
    the &#39;.md&#39; whereas writefreely anon posts typically do not).
  Modify this associative array for your situation. Include a trailing forward slash, /,
    in the $href element. Add a folder path prefix to the $fn filename if needed.
  The API data files should be in the same folder as this program.  
========================================================================================*/
$blog[0] = array(&#34;fn&#34; =&gt; &#34;wa-posts.json&#34;, &#34;href&#34; =&gt; &#34;https://write.as/&#34;, &#34;md_ext&#34; =&gt; &#39;.md&#39;);
$blog[1] = array(&#34;fn&#34; =&gt; &#34;qua-posts.json&#34;, &#34;href&#34; =&gt; &#34;https://my.writefreely.instance/&#34;, &#34;md_ext&#34; =&gt; &#39;&#39;);
$blog[2] = array(&#34;fn&#34; =&gt; &#34;wordsmith-posts.json&#34;, &#34;href&#34; =&gt; &#34;https://wordsmith.social/&#34;, &#34;md_ext&#34; =&gt; &#39;&#39;);
/*======================================================================================*/
    
/*======================================================================================
  The API data JSON files need to be downloaded before running this program. Here&#39;s a 
  curl command example for getting all user posts via the write.as or writefreely API:
    
  curl &#34;https://write.as/api/me/posts&#34; \
    -H &#34;Authorization: Token 00000000-1111-2222-3333-444444444444&#34; \
    -H &#34;Content-Type: application/json&#34; \
    -X GET  &gt;writeas_api_posts.json
    
  If you don&#39;t yet have an auth token, see this to get one:
  https://developers.write.as/docs/api/#authenticate-a-user
======================================================================================*/

// IMPORTANT NOTES:
//
// For anonymous posts, in order for the Title field to be populated in the API data,
// the first line of the post MUST start with a single # and a space. Otherwise, the 
// Title field will be blank.
//
// For the names of the elements provided by the API data, see bottom of this file
?&gt;

&lt;!DOCTYPE HTML&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta http-equiv=&#34;content-type&#34; content=&#34;text/html; charset=UTF-8&#34;&gt;
  &lt;title&gt;WaWf API Data Search&lt;/title&gt;
  &lt;style type=&#34;text/css&#34;&gt;
    body {
      font-family: sans-serif, helvetica;
      font-size: 15px;
      line-height: 1.3;
      margin: 15px;
    }
    a {
       color: blue;
       text-decoration: none;
     }
     a:hover {
       color: #AA08AA; 
       font-weight: bold;
       text-decoration: underline;
     }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt; 
&lt;?php
//==========================================
// Main part of program
//==========================================
$msg = &#39;Search these files: &#39;;
$i = 0;
foreach($blog as $item) {
  $fn = $item[&#39;fn&#39;];
  if (!file_exists($fn)) {
    echo &#34;&lt;span style=\&#34;color:red;\&#34;&gt;&lt;b&gt;Error: File $fn not found; please check and either correct filename or remove it from the list (i.e., the relevant &#34; . &#39;$blog[] entry in this program)&lt;/b&gt;&lt;/span&gt;&#39;;
    exit;
  } else {
  $msg .= $fn . &#34;, &#34;;
  }
  $href = $item[&#39;href&#39;];
  if (substr($href, -1) !== &#39;/&#39;) $blog[$i][&#39;href&#39;] = $href . &#39;/&#39;;
}  
echo substr($msg, 0, -2) . &#34;&lt;br&gt;&lt;br&gt;&#34;;
$search1=&#39;&#39;; $search2=&#39;&#39;; $search3=&#39;&#39;; $butnot=&#39;&#39;; $bOption=&#39;&#39;; $nq=&#39;&#39;;

if (isset($_POST[&#39;submit&#39;])) {
  $search1 = $_POST[&#39;searchkey1&#39;];
  $search2 = $_POST[&#39;searchkey2&#39;];
  $search3 = $_POST[&#39;searchkey3&#39;];
  $butnot =  $_POST[&#39;butnot&#39;];
  $bOption = $_POST[&#39;bOption&#39;];
  $uri = $_SERVER[&#39;REQUEST_URI&#39;];
  $nq = &#39;&lt;a href=&#34;&#39; . $uri . &#39;&#34;&gt;New Query&lt;/a&gt;&lt;br&gt;&#39;;
}

print &#39;&lt;form name=&#34;myform&#34; action=&#34;&#39; . $_SERVER[&#39;PHP_SELF&#39;] . &#39;&#34; method=&#34;post&#34;&gt;
	Keyword1: (&amp;nbsp;&lt;input name=&#34;searchkey1&#34; size=&#34;15&#34; autofocus value=&#34;&#39; . $search1 . &#39;&#34;&gt;&amp;nbsp;&lt;b&gt;AND&lt;/b&gt;
	Keyword2: &lt;input name=&#34;searchkey2&#34; size=&#34;15&#34; value=&#34;&#39; . $search2 . &#39;&#34;&gt;&amp;nbsp;)&amp;nbsp;
 	&lt;select name=&#34;bOption&#34;&gt;&#39;;
if ($bOption == &#39;OR&#39;) {
  print	&#39;&lt;option value=&#34;AND&#34;&gt;AND&lt;/option&gt;&lt;option value=&#34;OR&#34; selected&gt;OR&lt;/option&gt;&#39;;
} else {
  print	&#39;&lt;option value=&#34;AND&#34; selected&gt;AND&lt;/option&gt;&lt;option value=&#34;OR&#34;&gt;OR&lt;/option&gt;&#39;;
}  
print	&#39;&lt;/select&gt; 
	Keyword3: &lt;input name=&#34;searchkey3&#34; size=&#34;15&#34; value=&#34;&#39; . $search3 . &#39;&#34;&gt;&amp;nbsp;
	But Not: &lt;input name=&#34;butnot&#34; size=&#34;15&#34; value=&#34;&#39; . $butnot . &#39;&#34;&gt;&amp;nbsp;
	&lt;input type=&#34;submit&#34; name=&#34;submit&#34; value=&#34;Go&#34;&gt;&amp;nbsp;&amp;nbsp;&#39; . $nq . &#39;&lt;/form&gt;&#39;;
if (!isset($_POST[&#39;submit&#39;])) exit;

// The &#34;submit&#34; variable exists, so the form has been submitted - process form input and display results

// The API data file can have multiple blogs along with anonymous posts and they may be all mixed together with no 
// separation and no sort order. Therefore, if you want to have all anon posts and each blog&#39;s posts together in grouped
// sections, a sorting mechanism is needed. We sort on the url (which groups each blog&#39;s posts together&#39;) and to handle 
// the anon posts, we add a class=&#34;anon&#34; to the url so they&#39;ll be grouped together when sorted.

if ($search1 == &#34;&#34; &amp;&amp; $search2 == &#34;&#34;) {
  echo &#39;&lt;br&gt;&lt;b&gt;&lt;span style=&#34;color:red;&#34;&gt;You must input at least one search keyword in the first 2 input boxes&lt;/span&gt;&lt;/b&gt;&#39;;
  exit;
}

$iMatches = 0;
foreach($blog as $item) {
  $fn = $item[&#39;fn&#39;];
  $href = $item[&#39;href&#39;];
  $md_ext = $item[&#39;md_ext&#39;];
  $json = file_get_contents($fn);
  $array = json_decode($json, true);
  $blogposts = $array[&#39;data&#39;];
  $lines=&#39;&#39;;
  
  foreach ($blogposts as $post) {  
    $id    = $post[&#39;id&#39;];
    $slug  = $post[&#39;slug&#39;];
    $title = $post[&#39;title&#39;];
    // title might be blank; if so, substitute the slug if it&#39;s not also blank, otherwise use the post id
    if (trim($title) == &#39;&#39;) {
      if (trim($slug) !== &#39;&#39;) {
        $title = $slug;
      } else {
        $title = $id;
      }
    }
    $tags  = $post[&#39;tags&#39;];  // note $tags is an array
    $body  = $post[&#39;body&#39;];
    $url = &#39;&lt;a href=&#34;&#39; . $href . $id . &#39;.md&#34; target=&#34;_blank&#34;&gt;&#39; . &#34;&lt;b&gt;$title | $slug&lt;/b&gt;&lt;/a&gt;&lt;br&gt;\n&#34;; 
    //Check title, tags, and body of blog post for keyword matches - entire post vs individual lines?
    $contents = $title . &#39;||&#39; . implode(&#34; &#34;, $tags) . &#39;||&#39; . $body; //use || separator so don&#39;t have false match possibility due to a run-on 
    if (isMatched($contents)) {
      // Matched
      if (isset($post[&#39;collection&#39;])) {
        // Blog post
        $coll = $post[&#39;collection&#39;];
        $cAlias = $coll[&#39;alias&#39;];
        $blogUrl = &#39;&lt;a href=&#34;&#39; . $href .  $coll[&#39;alias&#39;] . &#39;&#34; target=&#34;_blank&#34;&gt;&#39; . $cAlias . &#34;&lt;/a&gt;&#34;; 
        $url = &#39;&lt;a href=&#34;&#39; . $href . $cAlias . &#39;/&#39; . $slug . &#39;&#34; target=&#34;_blank&#34;&gt;&#39; . &#34;&lt;b&gt;$title&lt;/b&gt;&lt;/a&gt;&#34;; 
        $lines .=  substr($cAlias,0,3) . &#34;$blogUrl | $url&lt;br&gt;\n&#34;;
      } else {
        // Anonymous posts
        // They typically have a null slug (possible exception is if they&#39;ve been moved from blog post to anonymous post)
        $url = &#39;&lt;a href=&#34;&#39; . $href . $id . $md_ext . &#39;&#34; target=&#34;_blank&#34;&gt;&#39; . &#34;&lt;b&gt;$title&lt;/b&gt;&lt;/a&gt;&lt;br&gt;\n&#34;;
        $lines .= &#39;ano&#39; . &#39;&lt;a class=&#34;anon&#34; href=&#34;&#39; . $href . &#39;me/posts&#34;&gt;Anonymous&lt;/a&gt;&#39; . &#34; | $url&lt;br&gt;\n&#34;;   //class=&#34;anon&#34; needed for sort
      }
      $iMatches += 1;
    }  
  } // end foreach blog post
  
  if ($iMatches == 0) {
    echo &#34;&lt;br&gt;No matches were found for the search criteria&#34;;
    exit;
  }
  
  // $lines contains each blog post. Sort them so all anon posts and separate blog posts group together.
  $posts = explode(&#34;\n&#34;, $lines);
  sort($posts, SORT_NATURAL | SORT_FLAG_CASE);
  $prev = &#39;&#39;;
  $i = 1;
  $itot = 0;
  foreach($posts as $p) {
    if(strlen($p) &gt; 4) {
      if (substr($p,0,3) !== $prev) {
        echo &#34;&lt;br&gt;\n&#34;;
        $i = 1;
      }  
      echo str_pad($i, 2, &#39;0&#39;, STR_PAD_LEFT) . &#34;: &#34; . substr($p,3) . &#34;\n&#34;;
      $i++;
      $itot += 1;
      $prev = substr($p, 0,3);
    }
  }
} // end foreach blog

echo &#34;&lt;br&gt;$iMatches matches were found for the search criteria&#34;;

function isMatched($contents) {
  global $search1;
  global $search2;
  global $search3;
  global $butnot;
  global $bOption;  //applies solely to the 3rd searchkey input

  if (stripos($contents, $butnot) !== false) return false;  
  if ($bOption == &#39;OR&#39; &amp;&amp; $search3 &gt; &#39;&#39; &amp;&amp; stripos($contents, $search3) !== false) return true;
 
  // The ButNot and OR criteria were handled above, so now we only need to check search 
  // term 1 by itself if it&#39;s the only input or in cobmination with any term 2 and 3 input
   if ($search1 == &#34;&#34; &amp;&amp; $search2 !== &#34;&#34;) {
  	$search1 = $search2;
  	$search2 = &#34;&#34;;
  }	
   if ($search2 == &#34;&#34; &amp;&amp; $search3 !== &#34;&#34; &amp;&amp; $bOption == &#34;AND&#34;) {
  	$search2 = $search3;
  	$search3 = &#34;&#34;;
  }	
  // By the program logic, search1 is guaranteed to be be populated
  // So if there is no search1 match and since only AND logic now applies here, return false
  if (stripos($contents,$search1) === false) return false;
  // We now know that there is a $search1 match
  if ($search2 == &#39;&#39; &amp;&amp; $search3 == &#39;&#39;) return true;
  if ($search2 &gt; &#39;&#39; &amp;&amp; stripos($contents, $search2) !== false  &amp;&amp; $search3 == &#39;&#39;) return true;
  if ($search2 &gt; &#39;&#39; &amp;&amp; stripos($contents, $search2) !== false  &amp;&amp; $search3 &gt; &#39;&#39; &amp;&amp; stripos($contents, $search3) !== false) return true;
  return false;
}

/*
Example elements in the JSON API data
  &#34;id&#34;: &#34;xxxxxxxxxx&#34;,
  &#34;slug&#34;: &#34;confidence-level-contours&#34;,
  &#34;appearance&#34;: &#34;sans&#34;,
  &#34;language&#34;: &#34;en&#34;,
  &#34;rtl&#34;: false,
  &#34;created&#34;: &#34;2019-11-03T15:46:58Z&#34;,
  &#34;updated&#34;: &#34;2019-11-03T16:09:50Z&#34;,
  &#34;title&#34;: &#34;Confidence level contours&#34;,
  &#34;body&#34;: &#34;Some body text&#34;,
  &#34;tags&#34;: [],
  &#34;images&#34;: [
     &#34;https://i.snap.as/xxxxxxx.jpg&#34;,
     &#34;https://i.snap.as/yyyyyyy.jpg&#34;,
   ],
  &#34;views&#34;: 0,
  &#34;collection&#34;: {
     &#34;alias&#34;: &#34;MyBlogName&#34;,
     &#34;title&#34;: &#34;My Blog Title&#34;,
     &#34;description&#34;: &#34;Primarily About this, that, and the other&#34;,
     &#34;style_sheet&#34;: &#34;&#34;,
     &#34;public&#34;: false,
     &#34;views&#34;: 22,
     &#34;total_posts&#34;: 0
     }
*/

?&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
]]></content:encoded>
      <guid>https://cloud.writeas.com/example-program-to-search-write-as-writefreely-api-data</guid>
      <pubDate>Sat, 14 Dec 2019 15:54:47 +0000</pubDate>
    </item>
    <item>
      <title>Google Compute Engine Upgrade from Debian 9 Stretch to Debian 10 Buster</title>
      <link>https://cloud.writeas.com/google-compute-engine-upgrade-from-debian-9-stretch-to-debian-10-buster?pk_campaign=rss-feed</link>
      <description>&lt;![CDATA[Background&#xA;I had installed a  Debian 9 Stretch GCE test single-user instance that I now wanted to upgrade to Buster. This is a simple f1-micro &#34;free tier&#34; instance with Apache2 and PHP7.3-FPM, Docker, and with no database other than sqlite3. Both Apache and PHP versions were the latest from the sury.org repositories. In searches, I found very little info on how to do the upgrade. Note that Google&#39;s Debian installs have some differences from standard Debian images.&#xA;&#xA;Some preparatory very helpful tips that I found are that (1) GCE instance boot disks can be attached and reattached, (2) a new persistent disk can be made from a saved snapshot of an instance&#39;s boot disk and then attached to the instance (basically swapping out the original boot disk with the new one created from the snapshot, and (3) a debian.org article on how to upgrade from version 9 to 10.&#xA;&#xA;With this info, I felt I had two upgrade choices:!--more--&#xA;&#xA;(1) Create a new GCE instance installing Debian 10, update it to match the configurations, apps, and data of the original v9 install, then  detach that persistent disk and attach it as a new boot disk on the original instance.  After verification that all was properly working, the second VM instance could be deleted along with the original boot disk with the old v9 install.&#xA;&#xA;(2) Make a snapshot of the current v9 disk, use it to create a new persistent disk, attach this new disk as the boot disk of the original instance, then try an upgrade and see if that would work ok. This would avoid having to create a second VM instance.&#xA;&#xA;I preferred the second option (the first one seemed like more work and I wanted to avoid starting over with reconfiguring a new install), though I found no write-ups online from anyone&#39;s experience trying that. But even if the upgrade in-place didn&#39;t work, I would still have the original v9 boot disk and could swap that back in if needed. So I went with the in-place upgrade.&#xA;&#xA;In-Place Upgrade Steps&#xA;So here&#39;s the basic steps I used and they worked without incident! (however, do note that this was a simple single-user instance with few apps).&#xA;&#xA;I basically followed the steps in the  debian.org doc on how to upgrade from version 9 to 10. Below I&#39;ll just mention any differences from those procedures.&#xA;&#xA;In addition to the backups suggested in Section 4.1.1, I backed up the entire webroot (/var/www/html/) folders and some other data and config files (e.g., in my home folder) to a Cloud Storage Bucket. I also would have the detached original boot disk available if needed. &#xA;&#xA;Section 4.1.6. Verify network interface name support was noted but I did nothing in response to it. I did have an eth0 network interface both before and after the upgrade, and there were no problems with it.&#xA;&#xA;The non-Debian sources I had were the sury.org packages, Docker, and the Google Cloud guest packages for Debian. As noted in the Debian upgrade doc, I updated all references from stretch to buster in /etc/apt/sources.source.list and to the files in /etc/apt/sources.list.d. Those were the only changes I made to these files. I noted that the /etc/apt/truteed.gpg.d folder included gpg keys for Buster, sury.org, and Google Cloud.&#xA;&#xA;I didn&#39;t record the session as recommended in Section 4.4.1, though in retrospect that would have been a prudent thing to do.&#xA;&#xA;As recommended in Section 4.4, I first did the &#39;minimal upgrade&#39;. This went without incident and 384 packages were updated. I then proceeded immediately (did not reboot) to the full-upgrade. 146 packages were upgraded, 69 newly installed, 1 was removed, and 0 not upgraded. There was a message that a number of packages were no longer required and could be removed with sudo apt autoremove, which I subsequently did after verifying that the upgrade was successful.&#xA;&#xA;There were only two prompts during the full-upgrade. One was whether to install a new /etc/ntp.conf or keep the old. After looking at the diffs and verifying that they were due to new statements and that I had not customized anything in the old, I selected the new version. The second prompt was whether to install the new /etc/sshdconfig. I kept the old as I had made customizations and there were no new statements in the v10 version that weren&#39;t in v9.&#xA;&#xA;I then rebooted the VM, and everything worked, no glitches were encountered. The new kernel is vmlinuz-4.19.0-6-amd64.&#xA;&#xA;As recommended in Section 4.7.1 Purging Removed Packages, I issued those commands. There were only 4 packages to be purged.&#xA;&#xA;There are 410 packages files in /var/cache/apt/archives/, most of them probably due to the upgrade. I will probably delete them at some point with apt clean.&#xA;&#xA;Updated Info Edits&#xA;After working with the new install for a while, there might be additional useful comments. If so, this post will be updated with a timestamp for the update added below.&#xA;&#xA;Original post version: 03-Dec-2019&#xA;&#xA;Hashtags: #Debian #GoogleComputeEngine  #Sysadmin #VPS]]&gt;</description>
      <content:encoded><![CDATA[<p><strong>Background</strong>
I had installed a  <a href="https://wiki.debian.org/DebianStretch" rel="nofollow">Debian 9 Stretch</a> GCE test single-user instance that I now wanted to upgrade to <a href="https://www.debian.org/releases/buster/" rel="nofollow">Buster</a>. This is a simple <a href="https://cloud.google.com/free/docs/gcp-free-tier" rel="nofollow">f1-micro “free tier”</a> instance with <a href="https://httpd.apache.org/" rel="nofollow">Apache2</a> and <a href="https://php-fpm.org/" rel="nofollow">PHP7.3-FPM</a>, Docker, and with no database other than sqlite3. Both Apache and PHP versions were the latest from the <a href="https://deb.sury.org/" rel="nofollow">sury.org</a> repositories. In searches, I found very little info on how to do the upgrade. Note that Google&#39;s Debian installs have some <a href="https://cloud.google.com/compute/docs/images#debian" rel="nofollow">differences from standard Debian images</a>.</p>

<p>Some preparatory very helpful tips that I found are that (1) GCE instance <a href="https://cloud.google.com/compute/docs/disks/detach-reattach-boot-disk" rel="nofollow">boot disks can be attached and reattached</a>, (2) a new <a href="https://cloud.google.com/compute/docs/disks/#repds" rel="nofollow">persistent disk</a> can be made from a saved <a href="https://cloud.google.com/compute/docs/disks/create-snapshots" rel="nofollow">snapshot</a> of an instance&#39;s boot disk and then attached to the instance (basically <a href="https://cloud.google.com/compute/docs/disks/restore-and-delete-snapshots" rel="nofollow">swapping out the original boot disk with the new one created from the snapshot</a>, and (3) a <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.html" rel="nofollow">debian.org article on how to upgrade from version 9 to 10</a>.</p>

<p>With this info, I felt I had two upgrade choices:</p>

<p>(1) Create a new GCE instance installing Debian 10, update it to match the configurations, apps, and data of the original v9 install, then  detach that persistent disk and attach it as a new boot disk on the original instance.  After verification that all was properly working, the second VM instance could be deleted along with the original boot disk with the old v9 install.</p>

<p>(2) Make a snapshot of the current v9 disk, use it to create a new persistent disk, attach this new disk as the boot disk of the original instance, then try an upgrade and see if that would work ok. This would avoid having to create a second VM instance.</p>

<p>I preferred the second option (the first one seemed like more work and I wanted to avoid starting over with reconfiguring a new install), though I found no write-ups online from anyone&#39;s experience trying that. But even if the upgrade in-place didn&#39;t work, I would still have the original v9 boot disk and could swap that back in if needed. So I went with the in-place upgrade.</p>

<p><strong>In-Place Upgrade Steps</strong>
So here&#39;s the basic steps I used and they worked without incident! (however, do note that this was a simple single-user instance with few apps).</p>

<p>I basically followed the steps in the  <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.html" rel="nofollow">debian.org doc on how to upgrade from version 9 to 10</a>. Below I&#39;ll just mention any differences from those procedures.</p>

<p>In addition to the backups suggested in <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html#data-backup" rel="nofollow">Section 4.1.1</a>, I backed up the entire webroot (<code>/var/www/html/</code>) folders and some other data and config files (e.g., in my home folder) to a <a href="https://codelabs.developers.google.com/codelabs/cloud-upload-objects-to-cloud-storage/#0" rel="nofollow">Cloud Storage Bucket</a>. I also would have the detached original boot disk available if needed.</p>

<p><a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html#review-interface-names" rel="nofollow">Section 4.1.6. Verify network interface name support</a> was noted but I did nothing in response to it. I did have an eth0 network interface both before and after the upgrade, and there were no problems with it.</p>

<p>The non-Debian sources I had were the <a href="https://deb.sury.org/" rel="nofollow">sury.org</a> packages, Docker, and the <a href="https://cloud.google.com/compute/docs/images/install-guest-environment#wgei_packages" rel="nofollow">Google Cloud guest packages for Debian</a>. As noted in the Debian upgrade doc, I updated all references from stretch to buster in <code>/etc/apt/sources.source.list</code> and to the files in <code>/etc/apt/sources.list.d</code>. Those were the only changes I made to these files. I noted that the <code>/etc/apt/truteed.gpg.d</code> folder included gpg keys for Buster, sury.org, and Google Cloud.</p>

<p>I didn&#39;t <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html#review-interface-names" rel="nofollow">record the session as recommended in Section 4.4.1</a>, though in retrospect that would have been a prudent thing to do.</p>

<p>As recommended in <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html#minimal-upgrade" rel="nofollow">Section 4.4</a>, I first did the &#39;minimal upgrade&#39;. This went without incident and 384 packages were updated. I then proceeded immediately (did not reboot) to the <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html#upgrading-full" rel="nofollow">full-upgrade</a>. 146 packages were upgraded, 69 newly installed, 1 was removed, and 0 not upgraded. There was a message that a number of packages were no longer required and could be removed with <code>sudo apt autoremove</code>, which I subsequently did after verifying that the upgrade was successful.</p>

<p>There were only two prompts during the full-upgrade. One was whether to install a new <code>/etc/ntp.conf</code> or keep the old. After looking at the diffs and verifying that they were due to new statements and that I had not customized anything in the old, I selected the new version. The second prompt was whether to install the new <code>/etc/sshd_config</code>. I kept the old as I had made customizations and there were no new statements in the v10 version that weren&#39;t in v9.</p>

<p>I then rebooted the VM, and everything worked, no glitches were encountered. The new kernel is <code>vmlinuz-4.19.0-6-amd64</code>.</p>

<p>As recommended in <a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-upgrading.en.html#purge-removed-packages" rel="nofollow">Section 4.7.1 Purging Removed Packages</a>, I issued those commands. There were only 4 packages to be purged.</p>

<p>There are 410 packages files in <code>/var/cache/apt/archives/</code>, most of them probably due to the upgrade. I will probably delete them at some point with <code>apt clean</code>.</p>

<p><strong>Updated Info Edits</strong>
After working with the new install for a while, there might be additional useful comments. If so, this post will be updated with a timestamp for the update added below.</p>

<p>Original post version: 03-Dec-2019</p>

<p>Hashtags: <a href="https://cloud.writeas.com/tag:Debian" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Debian</span></a> <a href="https://cloud.writeas.com/tag:GoogleComputeEngine" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">GoogleComputeEngine</span></a>  <a href="https://cloud.writeas.com/tag:Sysadmin" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">Sysadmin</span></a> <a href="https://cloud.writeas.com/tag:VPS" class="hashtag" rel="nofollow"><span>#</span><span class="p-category">VPS</span></a></p>
]]></content:encoded>
      <guid>https://cloud.writeas.com/google-compute-engine-upgrade-from-debian-9-stretch-to-debian-10-buster</guid>
      <pubDate>Thu, 12 Dec 2019 04:33:47 +0000</pubDate>
    </item>
  </channel>
</rss>