<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Tetua Community</title>
    <link>https://tetua.net/</link>
    <description>A network for software developers</description>
    <managingEditor>contact@tetua.net (Tetua Team)</managingEditor>
    <item>
      <title>Seznam doporucenych projektu a webovych sluzeb</title>
      <link>https://tetua.net/seznam-doporucenych-projektu-a-webovych-sluzeb-14.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>Globalni sit je obrovskym zdrojem informaci sluzeb a uzitecnych materialu. Vyhledavani kvalitniho obsahu byva narocne, ktere vas nezklamou. Prinasime vam proto aktualni souhrn, ktere se jiste hodi pro kazdodenni praci, studium i zabavu.</p>
<p>Pod timto textem nasleduje prehled odkazu – od diskuznich for po moderni aplikace.</p>
<p>Uzitecny internetovy проект - <a href="https://celostnilecba.com/faq/proces-formalizace-jazyka-v-informatice">https://celostnilecba.com/faq/proces-formalizace-jazyka-v-informatice</a><br />
Tato stranka nabizi sirokou skalu informaci a materialu pro bezne uzivatele. Clanky jsou dobre strukturovane, takze navstevnici vzdy dostanou aktualni data.</p>
<p>Prakticke webove utility - <a href="https://podhradem.net/sbirkytipu/televize-samsung-vysila-slaby-signal-co-delat">https://podhradem.net/sbirkytipu/televize-samsung-vysila-slaby-signal-co-delat</a><br />
Nabidka praktickych nastroju pro kazdodenni praci i zabavu. Nastroje jsou intuitivni a snadno ovladatelne.</p>
<p>Server s aktualnimi zpravami - <a href="https://arabiavacation.com/zkusenosti/odevzdani-prvniho-radku-v-excelu-jednoduche-zpusoby">https://arabiavacation.com/zkusenosti/odevzdani-prvniho-radku-v-excelu-jednoduche-zpusoby</a><br />
Zdroj aktualniho zpravodajstvi a zajimavosti ze sveta i domova. Struktura webu je logicka, coz usnadnuje vyhledavani konkretnich temat.</p>
<p>Progressivni online projekt - <a href="https://deponativ.info/zacatecnici/jak-vyrobit-reminek-na-hodinky-z-prave-kuze">https://deponativ.info/zacatecnici/jak-vyrobit-reminek-na-hodinky-z-prave-kuze</a><br />
Web nabizi moderni reseni a prehledne rozhrani. Skvele reseni pro moderni dobu.</p>
<p>Zajimava iniciativa - <a href="https://arabiavacation.com/napady/kde-v-dubnu-odpocivat-v-egypte">https://arabiavacation.com/napady/kde-v-dubnu-odpocivat-v-egypte</a><br />
Komunitni stranka sdruzujici lidi se stejnymi zajmy. Uzivatelska zakladna je aktivni, coz vytvari unikatni atmosferu.</p>
<p>Popularni internetovy portal - <a href="https://tvprodukce.com/overeno/jak-zakazat-oznameni-telegramu-na-obrazovce-android">https://tvprodukce.com/overeno/jak-zakazat-oznameni-telegramu-na-obrazovce-android</a><br />
Web s dlouhou tradici a tisici dennich navstevniku. Nabizi kombinaci zpravodajstvi a zabavy.</p>
<p>Uzitecny web do kapsy - <a href="https://celostnilecba.com/faq/prislovi-kde-je-tenko-tam-a-trha-se-vyznam-a-priklady-pouziti">https://celostnilecba.com/faq/prislovi-kde-je-tenko-tam-a-trha-se-vyznam-a-priklady-pouziti</a><br />
Prakticky pomocnik do kapsy. Prinasi rady a navody krok za krokem.</p>
<p>Online platforma pro inspiraci - <a href="https://arabiavacation.com/krokzakrokem/jak-krmit-hrozny-pred-kvetem-pomoci-lidovych-prostredku">https://arabiavacation.com/krokzakrokem/jak-krmit-hrozny-pred-kvetem-pomoci-lidovych-prostredku</a><br />
Misto, kde najdete nove podnety pro sve projekty. Uzitecne pro designery, marketery i kreativce.</p>
<p>Originalni web na siti - <a href="https://arabiavacation.com/krokzakrokem/pocet-linek-pci-express-na-procesoru-vse-co-potrebujete-vedet">https://arabiavacation.com/krokzakrokem/pocet-linek-pci-express-na-procesoru-vse-co-potrebujete-vedet</a><br />
Stranka s netradicnim pristupem k poskytovani obsahu. Autori dbaji na kvalitu a originalitu.</p>
<p>Kvalitni odborny zdroj - <a href="https://tvprodukce.com/seznamynapadu/prodlouzeni-doby-zhasnuti-obrazovky-pocitace-5-ucinnych-zpusobu-jak-usetrit-energii-a-prodlouzit-zivotnost-zarizeni">https://tvprodukce.com/seznamynapadu/prodlouzeni-doby-zhasnuti-obrazovky-pocitace-5-ucinnych-zpusobu-jak-usetrit-energii-a-prodlouzit-zivotnost-zarizeni</a><br />
Odborny portal pro pokrocile. Urceno pro lidi, kteri chteji jit do hloubky.</p>
<p>Statisticky prehled online - <a href="https://podhradem.net/rady/jednoduche-a-efektivni-zpusoby-jak-rozzarit-obrazovku-na-androidu-samsung">https://podhradem.net/rady/jednoduche-a-efektivni-zpusoby-jak-rozzarit-obrazovku-na-androidu-samsung</a><br />
Prehledne zobrazeni dat, statistik a grafu. Kvalitni podklad pro rozhodovani.</p>
<p>Sluzba pro moderni uzivatele - <a href="https://podhradem.net/jednoduche/jak-vycistit-optigril-tefal">https://podhradem.net/jednoduche/jak-vycistit-optigril-tefal</a><br />
Inovativni pristup k online sluzbam, ktery setri vas cas. Jednoducha registrace a okamzite vysledky.</p>
<p>Unikatni internetovy projekt - <a href="https://celostnilecba.com/praxe/jak-spravne-chranit-tulipany-a-narcisy-pred-chladem-a-mrazem-v-zime">https://celostnilecba.com/praxe/jak-spravne-chranit-tulipany-a-narcisy-pred-chladem-a-mrazem-v-zime</a><br />
Web, ktery vas nalaka na prvni pohled. Nabizi unikatni nastroje a funkce.</p>
<p>Online aplikace pro produktivitu - <a href="https://deponativ.info/otazky/jak-zkontrolovat-zapalovaci-civku-aveo-t300-multimetrem">https://deponativ.info/otazky/jak-zkontrolovat-zapalovaci-civku-aveo-t300-multimetrem</a><br />
Skvely pomocnik pro zvyseni produktivity prace. Vyhodnoti vas cas a navrhne zlepseni.</p>
<p>Tip na zajimavy web - <a href="https://tvprodukce.com/zapamatovat/sijeme-damskou-kosili-s-vlastnimi-vzory">https://tvprodukce.com/zapamatovat/sijeme-damskou-kosili-s-vlastnimi-vzory</a><br />
Posledni polozka v nasem seznamu, ktera si zaslouzi vasi pozornost. Univerzalni obsah pro vsechny generace.</p>
<p>Tato kolekce stranek bude skvelym zakladem pro praci i zabavu. Pokud hledate overene zdroje, jednoznacne navstivte nase doporuceni. Prejeme prijemne surfovani!</p>
<p>[url=http://forums.hrsrevamped.com/viewtopic.php?t=150205]Prehled uzitecnych internetovych stranek, ktere musite videt c88vfc[/url] fc275ef</p>
<p><a href="http://forums.shlock.co.uk/viewtopic.php?f=2&amp;t=449977">http://forums.shlock.co.uk/viewtopic.php?f=2&amp;t=449977</a><br />
<a href="http://w.mannlist.com/viewtopic.php?t=62513901">http://w.mannlist.com/viewtopic.php?t=62513901</a><br />
<a href="http://forums.shlock.co.uk/viewtopic.php?f=2&amp;t=449976">http://forums.shlock.co.uk/viewtopic.php?f=2&amp;t=449976</a><br />
<a href="http://apcboard.com/quote-1.html">http://apcboard.com/quote-1.html</a><br />
<a href="http://www.gm878.com/forum.php?mod=viewthread&amp;tid=306&amp;pid=41768&amp;page=24&amp;extra=page%3D1">http://www.gm878.com/forum.php?mod=viewthread&amp;tid=306&amp;pid=41768&amp;page=24&amp;extra=page%3D1</a></p>
]]></content:encoded>
      <author>ArielChasp</author>
      <guid>14</guid>
      <pubDate>Thu, 12 Mar 2026 11:50:38 +0000</pubDate>
    </item>
    <item>
      <title>You actually revealed this terrifically!</title>
      <link>https://tetua.net/you-actually-revealed-this-terrifically-13.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>casino games online [url=https://bhcmerced.org/#]usa online casino no deposit[/url]</p>
]]></content:encoded>
      <author>EdithBlups</author>
      <guid>13</guid>
      <pubDate>Tue, 17 Feb 2026 17:22:33 +0000</pubDate>
    </item>
    <item>
      <title>[b]Desire gorgeous female? [/b]</title>
      <link>https://tetua.net/b-desire-gorgeous-female-b-12.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>[b]Why not experience our selected trusted beauties![/b]<br />
[url=https://is.gd/fDIgDz][b]Get Them Right Now![/b][/url]</p>
]]></content:encoded>
      <author>DanielVeS</author>
      <guid>12</guid>
      <pubDate>Fri, 13 Feb 2026 11:44:16 +0000</pubDate>
    </item>
    <item>
      <title>[b]Want sexy woman? [/b]</title>
      <link>https://tetua.net/b-want-sexy-woman-b-11.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>[b]Simply test our trusted proven girls![/b]<br />
[url=https://is.gd/fDIgDz][b]Get Them Right Now![/b][/url]</p>
]]></content:encoded>
      <author>DanielVeS</author>
      <guid>11</guid>
      <pubDate>Mon, 02 Feb 2026 02:54:35 +0000</pubDate>
    </item>
    <item>
      <title>You actually mentioned it adequately.</title>
      <link>https://tetua.net/you-actually-mentioned-it-adequately-10.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>[url=https://tvprodukce.com/vedet/mikrofony-vokalni-se-electronics-vyberte-si-perfektni-provedeni]click here to read[/url]</p>
<p><a href="https://celostnilecba.com/triky/jak-pripojit-chytre-hodinky-k-androidu-samsung">https://celostnilecba.com/triky/jak-pripojit-chytre-hodinky-k-androidu-samsung</a><br />
<a href="https://deponativ.info/otazky/jak-se-spravne-zbavit-ikony-tipy-a-triky">https://deponativ.info/otazky/jak-se-spravne-zbavit-ikony-tipy-a-triky</a><br />
<a href="https://celostnilecba.com/tipy/co-je-supinator-v-botach-a-proc-je-potreba">https://celostnilecba.com/tipy/co-je-supinator-v-botach-a-proc-je-potreba</a><br />
<a href="https://celostnilecba.com/tipy/datum-vydani-operacniho-systemu-windows-10">https://celostnilecba.com/tipy/datum-vydani-operacniho-systemu-windows-10</a><br />
<a href="https://deponativ.info/postupy/ucinky-vyzivy-proteinem-misto-bezneho-jidla-pro-telo">https://deponativ.info/postupy/ucinky-vyzivy-proteinem-misto-bezneho-jidla-pro-telo</a><br />
<a href="https://deponativ.info/zacatecnici/jak-obnovit-rozpadajici-se-bios-na-notebooku-podrobny-navod">https://deponativ.info/zacatecnici/jak-obnovit-rozpadajici-se-bios-na-notebooku-podrobny-navod</a><br />
<a href="https://arabiavacation.com/zkusenosti/co-je-lepsi-pro-vykaly-sterk-nebo-sut">https://arabiavacation.com/zkusenosti/co-je-lepsi-pro-vykaly-sterk-nebo-sut</a><br />
<a href="https://deponativ.info/recenze/jak-se-zbavit-vlhkych-sten-v-dome">https://deponativ.info/recenze/jak-se-zbavit-vlhkych-sten-v-dome</a><br />
<a href="https://deponativ.info/recenze/jak-nastavit-notebook-uzitecne-tipy-a-pokyny">https://deponativ.info/recenze/jak-nastavit-notebook-uzitecne-tipy-a-pokyny</a><br />
<a href="https://arabiavacation.com/napady/co-se-stane-kdyz-upustite-iphone-7-na-podlahu">https://arabiavacation.com/napady/co-se-stane-kdyz-upustite-iphone-7-na-podlahu</a><br />
<a href="https://podhradem.net/sbirkytipu/cim-oblozit-verandu-v-soukromem-dome-venku-prakticka-a-stylova-reseni-pro-vytvoreni-utulneho-prostoru">https://podhradem.net/sbirkytipu/cim-oblozit-verandu-v-soukromem-dome-venku-prakticka-a-stylova-reseni-pro-vytvoreni-utulneho-prostoru</a><br />
<a href="https://deponativ.info/postupy/ve-snu-se-mi-zdalo-o-neobvyklych-podminkach">https://deponativ.info/postupy/ve-snu-se-mi-zdalo-o-neobvyklych-podminkach</a><br />
<a href="https://tvprodukce.com/overeno/jaky-by-mel-byt-pocet-hodin-telocviku-ve-skole-v-dnesnim-svete">https://tvprodukce.com/overeno/jaky-by-mel-byt-pocet-hodin-telocviku-ve-skole-v-dnesnim-svete</a><br />
<a href="https://deponativ.info/otazky/jak-zkontrolovat-penize-metody-a-tipy">https://deponativ.info/otazky/jak-zkontrolovat-penize-metody-a-tipy</a><br />
<a href="https://podhradem.net/rady/gpon-mgts-pristup-k-routeru">https://podhradem.net/rady/gpon-mgts-pristup-k-routeru</a></p>
<p>[url=https://forum.dylzo.ru/topic/60855-consultant-x95lwx/]consultant x95lwx[/url] fbfdfc2</p>
]]></content:encoded>
      <author>EdithBlups</author>
      <guid>10</guid>
      <pubDate>Sun, 09 Nov 2025 21:11:16 +0000</pubDate>
    </item>
    <item>
      <title>How to return an error in Golang</title>
      <link>https://tetua.net/how-to-return-an-error-in-golang-9.html</link>
      <description></description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Golang is a programming language created by Google. It is a statically typed language with syntax similar to that of C. It is open source and available for download.</p>
<p>One of the main features of Golang is that it is a compiled language, which means that it is converted into machine code that can be run on a computer. This makes Golang programs very fast and efficient.</p>
<p>However, Golang is not without its errors. One of the most common errors that occur in Golang is the &quot;undefined: X&quot; error. This error occurs when a variable or function is used that has not been defined.</p>
<p>There are a few ways to fix this error. The first is to simply define the variable or function that is being used. The second is to use a type assertion to convert the variable to the correct type. The &quot;undefined: X&quot; error can be frustrating, but it is relatively easy to fix.</p>
<h2 id="what-is-an-error-in-go">What is an error in Go?</h2>
<p>Error is a built-in type in golang. It is used to represent an error condition, and is used to indicate failure of an operation. Errors are represented by the built-in type <code>error</code>. The built-in type <code>error</code> is a struct type with a single field, <code>error</code>, of type <code>string</code>. The <code>error</code> field is a string that describes the error.</p>
<p>An error in Go is defined as a value that implements the error interface. The error interface is a built-in interface in Go that is defined as follows:</p>
<pre><code class="language-go">type error interface {
    Error() string
}
</code></pre>
<p>The error interface is used to indicate an exceptional condition that has occurred. When an error occurs, it is often useful to have some context about what caused the error. The <code>Error()</code> string method provides that context.</p>
<p>The most common use of errors is to handle exceptional conditions when parsing text or binary data. For example, when reading a JSON document, a well-formed document must be encountered. If the document is not well-formed, an error will be returned.</p>
<p>Another common use of errors is to handle unexpected conditions when executing a program. For example, if a program tries to open a file that does not exist, an error will be returned.</p>
<p>In general, if a program encounters an error, it should be handled gracefully and not panic.</p>
<h2 id="how-to-return-an-error-in-go">How to return an error in Go?</h2>
<p>The Go programming language has built-in support for returning errors from functions. When a function returns an error, it is generally the last return value. The error type is a built-in interface:</p>
<pre><code class="language-go">type error interface {
    Error() string
}
</code></pre>
<p>An error can be returned in any case where a regular value would be returned. When the <code>os.Open</code> method fails to open a file, for example, it gives an error:</p>
<pre><code class="language-go">f, err := os.Open(&quot;filename.ext&quot;)
if err != nil {
    // handle error
}
// use f
</code></pre>
<p>The <code>err != nil</code> check is a common idiom in Go code. It is used to test whether the value of err is an error value or not. If it is, the code in the if block is executed.</p>
<p>There are many ways to create an error value. The most common is to use the New function from the errors package:</p>
<pre><code class="language-go">err := errors.New(&quot;something went wrong&quot;)
</code></pre>
<p>This creates an error with the message <strong>&quot;something went wrong&quot;</strong>. Another common way to create an error is to use the <code>fmt.Errorf</code> function:</p>
<pre><code class="language-go">err := fmt.Errorf(&quot;something %s&quot;, &quot;went wrong&quot;)
</code></pre>
<p>This creates an error with the message <strong>&quot;something went wrong&quot;</strong>. The <code>%s</code> is a format specifier that will be replaced with the value of the <strong>&quot;went wrong&quot;</strong> string.</p>
<p>Once you have an error value, you can return it from a function like any other value:</p>
<pre><code class="language-go">func doSomething() error {
    // ...
    return fmt.Errorf(&quot;something went wrong&quot;)
}
</code></pre>
<p>If you want to return an error and a regular value from a function, you can use the multiple assignment idiom:</p>
<pre><code class="language-go">func doSomething() (string, error) {
    // ...
    return &quot;&quot;, fmt.Errorf(&quot;something went wrong&quot;)
}
</code></pre>
<p>This idiom is often used when a function needs to return both an error and a result.</p>
<p>There are many functions in the Go standard library that return errors. For example, the <a href="http://os.Open">os.Open</a> function returns an error when it fails to open a file. When writing your own code, you should return errors wherever it makes sense to do so.</p>
]]></content:encoded>
      <author>Sergio</author>
      <guid>9</guid>
      <pubDate>Tue, 23 Aug 2022 03:50:18 +0000</pubDate>
    </item>
    <item>
      <title>[GO] Handle file uploading with Rclone in a Golang project</title>
      <link>https://tetua.net/go-handle-file-uploading-with-rclone-in-a-golang-project-4.html</link>
      <description></description>
      <content:encoded><![CDATA[<p><code>Rclone</code> is a great project that helps manage files across multi-cloud storage providers. This project has more than 32k stars on Github.</p>
<blockquote>
<p>Rclone is a command-line program to manage files on cloud storage. It is a feature-rich alternative to cloud vendors' web storage interfaces. <a href="https://rclone.org/#providers">Over 40 cloud storage products</a> support rclone including S3 object stores, business &amp; consumer file storage services, as well as standard transfer protocols.</p>
<p><a href="https://rclone.org/#about"><em>https://rclone.org/#about</em></a></p>
</blockquote>
<p>Rclone shipped as a command-line program and is easy for the end users to configure and run.</p>
<p>But what if you want to use the Rclone feature in your Golang project? Luckily, Rclone was written in Golang and it’s possible for us to directly use Rclone API in our project.</p>
<p>In this post, I will demonstrate the usage of the <code>Rclone API</code> to handle file uploading.</p>
<p>Assume that we already have an api that receives the <code>Multipart form request</code>, this api will take the request and expose a <code>*multipart.FileHeader</code> for us to use in the next step.</p>
<p>Because Rclone support lots of <code>Storage Provider</code> and we may want to use these providers in our code, we will need to create an abstract interface for the future implementation.</p>
<h2 id="define-the-type-and-interface">Define the type and interface</h2>
<pre><code class="language-go">import (
	&quot;context&quot;
	&quot;github.com/rclone/rclone/fs&quot;
	&quot;github.com/rclone/rclone/fs/object&quot;
)

type FileInfo struct {
	Disk string `json:&quot;disk,omitempty&quot;`
	Path string `json:&quot;path,omitempty&quot;`
	Type string `json:&quot;type,omitempty&quot;`
	Size int    `json:&quot;size,omitempty&quot;`
}

type Disk interface {
	Name() string
	Url(filepath string) string
	Delete(c context.Context, filepath string) error
	Put(c context.Context, in io.Reader, size int64, mime, dst string) (*FileInfo, error)
	Multipart(c context.Context, m *multipart.FileHeader, dsts ...string) (*FileInfo, error)
}

type RcloneDisk struct {
	fs.Fs
	DiskName string `json:&quot;name&quot;`
	Root     string
}

func (r *RcloneDisk) Name() string {
	return r.DiskName
}
</code></pre>
<ul>
<li><code>Disk</code> is an interface that we will use later</li>
<li><code>FileInfo</code> holds the uploaded file information</li>
<li><code>RcloneDisk</code> is a base implementation of the Disk interface, the future disks we create will extend from this base</li>
</ul>
<p>Take a look at <a href="https://github.com/rclone/rclone/blob/master/fs/types.go#L46">Rclone Fs interface</a> and we have this:</p>
<pre><code class="language-go">type Fs interface {
...
// Put in to the remote path with the modTime given of the given size
//
// When called from outside an Fs by rclone, src.Size() will always be &gt;= 0.
// But for unknown-sized objects (indicated by src.Size() == -1), Put should either
// return an error or upload it properly (rather than e.g. calling panic).
//
// May create the object even if it returns an error - if so
// will return the object and the error, otherwise will return
// nil and the error
Put(ctx context.Context, in io.Reader, src ObjectInfo, options ...OpenOption) (Object, error)
</code></pre>
<p><code>Rclone</code> already implements the <code>Fs</code> interface for each supported provider, the full list can be found <a href="https://github.com/rclone/rclone/tree/master/backend">here</a></p>
<h2 id="create-the-put-method-for-the-base-struct-rclonedisk">Create the Put method for the base struct RcloneDisk</h2>
<pre><code class="language-go">func (r *RcloneDisk) Put(ctx context.Context, reader io.Reader, size int64, mime, dst string) (*FileInfo, error) {
	objectInfo := object.NewStaticObjectInfo(
		dst,
		time.Now(),
		size,
		true,
		nil,
		nil,
	)

	rs, err := r.Fs.Put(ctx, reader, objectInfo)

	if err != nil {
		return nil, err
	}

	return &amp;FileInfo{
		Disk: r.DiskName,
		Path: dst,
		Type: mime,
		Size: int(rs.Size()),
	}, nil
}
</code></pre>
<p>As defined in the <code>Rclone Fs interface</code>, the <code>Put</code> method receives these parameters:</p>
<ul>
<li><code>ctx</code>: A context for easily controlling the method behavior</li>
<li><code>reader</code>: An <code>io.Reader</code> object that holds the file upload reader, we will get this reader from the <code>*multipart.FileHeader</code></li>
<li><code>src</code>: A <code>rclonefs.ObjectInfo</code> object that holds the file information. In order to create this object will have to collect some additional information as file size, upload destination</li>
</ul>
<h2 id="create-the-multipart-method-for-the-base-struct-rclonedisk">Create the Multipart method for the base struct RcloneDisk</h2>
<pre><code class="language-go">func (r *RcloneDisk) Multipart(ctx context.Context, m *multipart.FileHeader, dsts ...string) (*FileInfo, error) {
	f, err := m.Open()

	if err != nil {
		return nil, err
	}

	fileHeader := make([]byte, 512)

	if _, err := f.Read(fileHeader); err != nil {
		return nil, err
	}

	if _, err := f.Seek(0, 0); err != nil {
		return nil, err
	}

	dst := &quot;&quot;
	mime := http.DetectContentType(fileHeader)

	if len(dsts) &gt; 0 {
		dst = dsts[0]
	} else {
		dst = r.UploadFilePath(m.Filename)
	}

	return r.Put(ctx, f, m.Size, mime, dst)
}
</code></pre>
<h3 id="create-the-default-file-path-for-every-upload">Create the default file path for every upload</h3>
<pre><code class="language-go">func (r *RcloneDisk) UploadFilePath(filename string) string {
    var filenameRemoveCharsRegexp = regexp.MustCompile(`[^a-zA-Z0-9_\-\.]`)
    var dashRegexp = regexp.MustCompile(`\-+`)
	now := time.Now()
	filename = filenameRemoveCharsRegexp.ReplaceAllString(filename, &quot;-&quot;)
	filename = dashRegexp.ReplaceAllString(filename, &quot;-&quot;)
	filename = strings.ReplaceAll(filename, &quot;-.&quot;, &quot;.&quot;)
	return path.Join(
		r.Root,
		strconv.Itoa(now.Year()),
		fmt.Sprintf(&quot;%02d&quot;, int(now.Month())),
		fmt.Sprintf(&quot;%d_%s&quot;, now.UnixMicro(), filename),
	)
}

func (r *RcloneDisk) Delete(ctx context.Context, filepath string) error {
	obj, err := r.Fs.NewObject(ctx, filepath)

	if err != nil {
		return err
	}

	return obj.Remove(ctx)
}
</code></pre>
<p><em>Note:</em></p>
<p>Because will Read the file to check its mime <code>f.Read(fileHeader)</code>, we have to seek it at the start of the io.Reader to avoid file corruption: <code>f.Seek(0, 0)</code></p>
<p><strong>The basic implementation of the RcloneDisk is done, we will create the S3 Disk and the Local Disk that extend this base disk in the steps bellow.</strong></p>
<h2 id="create-the-local-disk-save-file-to-the-local-storage">Create the local disk: Save file to the local storage</h2>
<pre><code class="language-go">type RcloneLocal struct {
	*RcloneDisk
	Root      string        `json:&quot;root&quot;`
	BaseUrl   string        `json:&quot;base_url&quot;`
}

func (r *RcloneLocal) Url(filepath string) string {
	return r.BaseUrl + &quot;/&quot; + filepath
}
</code></pre>
<p>This struct contains extra properties:</p>
<ul>
<li><code>Root</code>: The root directory for the upload</li>
<li><code>Baseurl</code>: The base url for the public file url</li>
</ul>
<h3 id="create-the-local-disk">Create the local disk:</h3>
<pre><code class="language-go">import (
	&quot;context&quot;
	&quot;os&quot;

	&quot;github.com/rclone/rclone/backend/local&quot;
	&quot;github.com/rclone/rclone/fs/config/configmap&quot;
)

func NewLocal() Disk {
	rl := &amp;RcloneLocal{
		BaseRcloneDisk: &amp;BaseRcloneDisk{
			DiskName: &quot;local_disk&quot;,
		},
		Root:      &quot;/var/www/html/files&quot;,
		BaseUrl:   &quot;/files&quot;,
	}

	cfgMap := configmap.New()
	cfgMap.Set(&quot;root&quot;, rl.Root)
	fsDriver, err := local.NewFs(context.Background(), rl.DiskName, rl.Root, cfgMap)

	if err != nil {
		panic(err)
	}

	rl.Fs = fsDriver

	return rl
}
</code></pre>
<p>In this code, we import the Local backend that implemented the <code>Rclone Fs</code>: <code>&quot;github.com/rclone/rclone/backend/local&quot;</code></p>
<h2 id="create-the-s3-disk-save-file-to-aws-s3-or-s3-compatible-storage-providers">Create the S3 disk: Save file to AWS S3 or S3 compatible storage providers</h2>
<pre><code class="language-go">type RcloneS3 struct {
	*RcloneDisk
	Root            string              `json:&quot;root&quot;`
	Provider        string              `json:&quot;provider&quot;`
	Bucket          string              `json:&quot;bucket&quot;`
	Region          string              `json:&quot;region&quot;`
	Endpoint        string              `json:&quot;endpoint&quot;`
	ChunkSize       fs.SizeSuffix       `json:&quot;chunk_size&quot;`
	AccessKeyID     string              `json:&quot;access_key_id&quot;`
	SecretAccessKey string              `json:&quot;secret_access_key&quot;`
	BaseUrl         string              `json:&quot;base_url&quot;`
	ACL             string              `json:&quot;acl&quot;`
}

func (r *RcloneS3) Url(filepath string) string {
	return r.BaseUrl + filepath
}
</code></pre>
<h3 id="create-the-s3-disk">Create the S3 disk</h3>
<pre><code class="language-go">import (
	&quot;context&quot;

	&quot;github.com/rclone/rclone/backend/s3&quot;
	&quot;github.com/rclone/rclone/fs&quot;
	&quot;github.com/rclone/rclone/fs/config/configmap&quot;
)


func NewS3(cfg *RcloneS3Config) fs.FSDisk {
	rs3 := &amp;RcloneS3{
		BaseRcloneDisk: &amp;BaseRcloneDisk{
			DiskName: &quot;DO_DISK&quot;,
			Root:     &quot;/files&quot;,
		},
		Root:            &quot;/files&quot;,
		Provider:        &quot;DigitalOcean&quot;,
		Region:          &quot;sfo3&quot;,
		Endpoint:        &quot;sfo3.digitaloceanspaces.com&quot;,
		ChunkSize:       rclonefs.SizeSuffix(1024 * 1024 * 5),
		AccessKeyID:     &quot;AccessKeyID&quot;,
		SecretAccessKey: &quot;SecretAccessKey&quot;,
		BaseUrl:         &quot;https://cdn.mysite&quot;,
		ACL:             &quot;public-read&quot;,
	}

	cfgMap := &amp;configmap.Simple{}
	cfgMap.Set(&quot;provider&quot;, rs3.Provider)
	cfgMap.Set(&quot;bucket&quot;, rs3.Bucket)
	cfgMap.Set(&quot;region&quot;, rs3.Region)
	cfgMap.Set(&quot;endpoint&quot;, rs3.Endpoint)
	cfgMap.Set(&quot;chunk_size&quot;, rs3.ChunkSize.String())
	cfgMap.Set(&quot;access_key_id&quot;, rs3.AccessKeyID)
	cfgMap.Set(&quot;secret_access_key&quot;, rs3.SecretAccessKey)
	cfgMap.Set(&quot;acl&quot;, rs3.ACL)
	cfgMap.Set(&quot;bucket_acl&quot;, rs3.ACL)

	fsDriver, err := s3.NewFs(context.Background(), &quot;s3&quot;, rs3.Bucket, cfgMap)

	if err != nil {
		panic(err)
	}

	rs3.Fs = fsDriver

	return rs3
}
</code></pre>
<p>In this code, we import the S3 backend that implemented the <code>Rclone Fs</code>: <code>&quot;github.com/rclone/rclone/backend/s3&quot;</code></p>
<h2 id="usage">Usage</h2>
<pre><code class="language-go">var localDisk = NewLocal()
var s3Disk = NewS3()

func Upload(c server.Context) error {
    ctx := context.Background()
    fileHeader := c.File(&quot;file&quot;) // *multipart.FileHeader
    localDisk.Multipart(ctx, fileHeader) // Save file to the local storage
    s3Disk.Multipart(ctx, fileHeader) // Save file to the S3 compatible storage
}
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>In this post, we just talked about the File Saving process using Rclone, there are many <a href="https://rclone.org/#features">features that Rclone offers</a>. By looking into the source code we can directly use Rclone APIs without using its binary.</p>
<p>Thank you for spending time on my post, if you have any questions, don’t hesitate to leave a comment here!</p>
]]></content:encoded>
      <author>Ngoc Phuong</author>
      <guid>4</guid>
      <pubDate>Sat, 14 May 2022 04:56:45 +0000</pubDate>
    </item>
    <item>
      <title>[GO] Some Slice helper functions using Generic</title>
      <link>https://tetua.net/go-some-slice-helper-functions-using-generic-3.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>When developing application with Golang, i sometime have to write helper functions for that working with slice. There are common functions that i use frequence as:</p>
<ul>
<li>Check if slice contain an element</li>
<li>Filter out slice element that match some condition</li>
<li>Map a slice to a diffrence type</li>
</ul>
<p>It was a common case that i have to rewrite every helper for difference type of data, and it’s so annoy.</p>
<p>With the recent version 18 of Go, i decided to rewrite the common slice function with the help of Generic. As an expected result, generic work good and help me avoid the redundant/dry code.</p>
<p>Here are some slice helper functions that i want to share:</p>
<h3 id="contains-check-if-a-slice-contain-an-element">Contains: Check if a slice contain an element</h3>
<pre><code class="language-go">func Contains[T comparable](slice []T, element T) bool {
	for _, e := range slice {
		if e == element {
			return true
		}
	}

	return false
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">var stringSlice = []string{&quot;item 1&quot;, &quot;item 2&quot;}
var intSlice = []int{1, 2, 3}

fmt.Println(Contains(stringSlice, &quot;item 2&quot;)) // true
fmt.Println(Contains(intSlice, 4)) // false
</code></pre>
<h3 id="filter-filter-a-slice-to-return-only-the-elements-that-match-a-condition">Filter: Filter a slice to return only the elements that match a condition</h3>
<pre><code class="language-go">func Filter[T comparable](slice []T, predicate func(T) bool) []T {
	var result []T
	for _, e := range slice {
		if predicate(e) {
			result = append(result, e)
		}
	}

	return result
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">var intSlice = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

var oddSlice = Filter(intSlice, func(element int) bool {
    return element % 2 != 0
})

fmt.Println(oddSlice) // []int{1, 3, 5, 7, 9}
</code></pre>
<h3 id="map-transform-a-slice-into-a-diffrence-slice-type">Map: Transform a slice into a diffrence slice type</h3>
<pre><code class="language-go">func Map[T comparable, R comparable](slice []T, mapper func(T) R) []R {
	var result []R
	for _, e := range slice {
		result = append(result, mapper(e))
	}

	return result
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">var intSlice = []int{1, 2, 3}
var strSlice = Map(intSlice, func(element int) string {
    return strconv.Itoa(element)
})

fmt.Println(strSlice) // []string{&quot;1&quot;, &quot;2&quot;, &quot;3&quot;}
</code></pre>
<h3 id="repeat-create-a-slice-with-a-repeated-values">Repeat: Create a slice with a repeated values</h3>
<pre><code class="language-go">func Repeat[T comparable](input T, time int) []T {
	var result []T
	var i = 0
	for i &lt; time {
		result = append(result, input)
		i++
	}

	return result
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">var intSlice = Repeat(1, 3)

fmt.Println(intSlice) // []int{1, 1, 1}
</code></pre>
<h3 id="overlap-get-the-overlap-elements-beetween-two-slice">Overlap: Get the overlap elements beetween two slice</h3>
<pre><code class="language-go">func Overlap[T comparable](slice1 []T, slice2 []T) []T {
	result := make([]T, 0)
	for _, e1 := range slice1 {
		if SliceContains(slice2, e1) {
			result = append(result, e1)
		}
	}

	return result
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">var slice1 = []int{1, 2, 3}
var slice2 = []int{2, 3, 4}
var result = Overlap(slice1, slice2)

fmt.Println(result) // []int{2, 3}
</code></pre>
<h3 id="appendifnotexists-append-an-element-to-a-slice-only-if-it-isnt-existed">AppendIfNotExists: Append an element to a slice only if it isn’t existed</h3>
<pre><code class="language-go">func AppendIfNotExists[T comparable](slice []T, newItem T) []T {
	for _, s := range slice {
		if s == newItem {
			return slice
		}
	}

	return append(slice, newItem)
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">var intSlice = []int{1, 2, 3}
var intSlice1 = AppendIfNotExists(intSlice, 3)
var intSlice2 = AppendIfNotExists(intSlice, 4)

fmt.Println(intSlice1) // []int{1, 2, 3}
fmt.Println(intSlice2) // []int{1, 2, 3, 4}
</code></pre>
<p>Some time you may want to append an item to a slice only if a certain condition is matched or the <code>Type</code> is not comparable, you can modify the <code>AppendIfNotExists</code> as bellow:</p>
<pre><code class="language-go">func AppendIfNotExists[T any](slice []T, newItem T, checkExists func(T) bool) []T {
	for _, s := range slice {
		if checkExists(s) {
			return slice
		}
	}

	return append(slice, newItem)
}
</code></pre>
<p><strong>Usage</strong></p>
<pre><code class="language-go">type Thing struct {
    ID int
    Name string
}

var things = []Thing{
	{ID: 1, Name: &quot;one&quot;},
	{ID: 2, Name: &quot;two&quot;},
}

var thing3 = Thing{ID: 3, &quot;three&quot;}
var thing2 = Thing{ID: 2, Name: &quot;Thing 2&quot;}

var thingSlice1 = AppendIfNotExists(things, thing2, func (element Thing) bool {
    return element.ID == thing2.ID
})

var thingSlice2 = AppendIfNotExists(things, thing3, func (element Thing) bool {
    return element.ID == thing3.ID
})

fmt.Println(len(thingSlice1)) // 2
fmt.Println(len(thingSlice2)) // 3
</code></pre>
]]></content:encoded>
      <author>Ngoc Phuong</author>
      <guid>3</guid>
      <pubDate>Sat, 14 May 2022 04:16:25 +0000</pubDate>
    </item>
    <item>
      <title>Deploy Tetua with docker</title>
      <link>https://tetua.net/deploy-tetua-with-docker-2.html</link>
      <description></description>
      <content:encoded><![CDATA[<p>Tetua can be set up manually following the instructions in the <a href="https://tetua.net/tetua-getting-started-1.html">getting started</a> section. Docker is another option for installation.</p>
<p>There are things that will be guided in this section:</p>
<ul>
<li>Install Docker</li>
<li>Deploy Portainer container</li>
<li>Deploy Tetua container</li>
<li>Deploy Nginx container</li>
</ul>
<h2 id="install-docker">Install Docker</h2>
<p>Follow these links for the detailed document on how to install <code>Docker</code> and <code>docker-compose</code>:</p>
<h2 id="deploy-portainer-container">Deploy Portainer container</h2>
<p>Create portainer docker-compose file:</p>
<pre><code class="language-bash">mkdir -p /opt/containers/portainer/data
touch /opt/containers/docker-compose.yml
</code></pre>
<p>Update file <code>/opt/containers/docker-compose.yml</code> with the following content:</p>
<pre><code class="language-yaml">version: '3'

services:
  portainer:
    image: portainer/portainer:latest
    container_name: portainer
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true
    networks:
      - proxy
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data:/data

networks:
  proxy:
    external: true
</code></pre>
<p>Run the portainer container</p>
<pre><code class="language-bash">cd /opt/containers/portainer
docker-compose up -d
</code></pre>
<h2 id="deploy-tetua-container">Deploy Tetua container</h2>
<p>Download and extract the Tetua binary:</p>
<pre><code class="language-bash">mkdir -p /opt/containers/tetua
cd /opt/containers/tetua
wget https://github.com/ngocphuongnb/tetua/releases/download/v0.0.3-alpha/tetua_0.0.3-alpha_Linux_x86_64.tar.gz
tar -xvf tetua_0.0.3-alpha_Linux_x86_64.tar.gz
</code></pre>
<p>Create <code>/opt/containers/Dockerfile</code> with this content;:</p>
<pre><code class="language-yaml">FROM ubuntu:bionic

ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -yq ca-certificates
RUN apt-get install tzdata -y

WORKDIR /tetua


COPY ./tetua /tetua/tetua

EXPOSE 3000

CMD ./tetua run
</code></pre>
<p>Create <code>/opt/containers/docker-compose.yml</code> with this content:</p>
<pre><code class="language-yaml">version: '3'

services:
  tetua:
    container_name: tetua
    image: tetua
    restart: always
    build:
      context: .
      dockerfile: ./Dockerfile
    environment:
      APP_ENV: production
    volumes:
      - ./config.json:/tetua/config.json
      - ./public:/tetua/public
      - ./private:/tetua/private
    networks:
      - proxy

networks:
  proxy:
    external: true
</code></pre>
<p>Create the Tetua config file:</p>
<pre><code class="language-bash">./tetua init
./tetua setup -u admin -p password
</code></pre>
<p>Run the Tetua container</p>
<pre><code class="language-bash">cd /opt/containers/tetua
docker-compose up -d
</code></pre>
<h2 id="deploy-nginx-container">Deploy Nginx container</h2>
<pre><code class="language-bash">mkdir -p /opt/containers/nginx/conf.d
</code></pre>
<p>Create file <code>/opt/containers/nginx/docker-compose.yml</code> with this content</p>
<pre><code class="language-yaml">version: '3'

services:
  nginx:
    container_name: nginx
    image: nginx
    restart: always
    volumes:
      - ./conf.d/portainer.nginx.conf:/etc/nginx/conf.d/portainer.nginx.conf
      - ./conf.d/tetua.nginx.conf:/etc/nginx/conf.d/tetua.nginx.conf
    ports:
      - 80:80
    command: /bin/sh -c &quot;exec nginx -g 'daemon off;'&quot;
    networks:
      - proxy

networks:
  proxy:
    external: true
</code></pre>
<p>Create file <code>/opt/containers/nginx/conf.d/portainer.nginx.conf</code> with this content:</p>
<pre><code class="language-null">upstream app_portainer {
  server portainer:9000;
}

server {
  listen 80;
  server_name portainer.mytetuaapp.local;
  root /data;

  access_log /var/log/nginx/nginx.portainer.log;
  error_log /var/log/nginx/nginx.portainer.error.log;

  location / {
    index index.html index.htm;
    try_files $uri @portainer;
  }

  location @portainer {
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_pass http://app_portainer;
    add_header X-Cache-Status $upstream_cache_status;
  }
}
</code></pre>
<p>Create file <code>/opt/containers/nginx/conf.d/tetua.nginx.conf</code> with this content:</p>
<pre><code>upstream app_tetua {
  server tetua:3000;
}

server {
  listen 80;
  server_name mytetuaapp.local;

  access_log /var/log/nginx/nginx.tetua.log;
  error_log /var/log/nginx/nginx.tetua.error.log;

  location / {
    index index.html index.htm;
    try_files $uri @tetua;
  }

  location @tetua {
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_pass http://app_tetua;
    add_header X-Cache-Status $upstream_cache_status;
  }
}
</code></pre>
<p>Run the Nginx container</p>
<pre><code class="language-bash">cd /opt/containers/nginx
docker-compose up -d
</code></pre>
<p>You can now be able to access the Portainer dashboard to manage the server containers: <code>https://portainer.mytetuaapp.local</code></p>
<p>And the Tetua app will be available at <code>https://mytetuapp.local</code></p>
]]></content:encoded>
      <author>Ngoc Phuong</author>
      <guid>2</guid>
      <pubDate>Fri, 13 May 2022 00:12:20 +0000</pubDate>
    </item>
    <item>
      <title>Tetua getting started</title>
      <link>https://tetua.net/tetua-getting-started-1.html</link>
      <description></description>
      <content:encoded><![CDATA[<h2 id="installation">Installation</h2>
<p>Tetua provide binary packages for easy setting up and running a blog, these are supported plattform:</p>
<ul>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Darwin_arm64.tar.gz">Darwin_arm64</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Darwin_x86_64.tar.gz">Darwin_x86_64</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Linux_arm64.tar.gz">Linux_arm64</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Linux_i386.tar.gz">Linux_i386</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Linux_x86_64.tar.gz">Linux_x86_64</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Windows_arm64.tar.gz">Windows_arm64</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Windows_i386.tar.gz">Windows_i386</a></li>
<li><a href="https://github.com/ngocphuongnb/tetua/releases/download/v0.0.1-alpha/tetua_0.0.1-alpha_Windows_x86_64.tar.gz">Windows_x86_64</a></li>
</ul>
<p>To run a Tetua blog, you have to download and and execute the binary.<br />
The current version is <code>v0.0.5-alpha</code> so if you want to deploy a blog, the process is as following:</p>
<p><strong>Download and extract the binary</strong></p>
<pre><code class="language-bash">mkdir -p /home/myblog
wget https://github.com/ngocphuongnb/tetua/releases/download/v0.0.3-alpha/tetua_0.0.3-alpha_Linux_x86_64.tar.gz
tar -xvf tetua_0.0.5-alpha_Linux_x86_64.tar.gz
</code></pre>
<p><strong>Create the config file</strong></p>
<pre><code class="language-bash">./tetua init
</code></pre>
<p>The config file will be created in the current directory:</p>
<pre><code class="language-json">{
 &quot;app_env&quot;: &quot;production&quot;,
 &quot;app_key&quot;: &quot;{APP_KEY}&quot;,
 &quot;app_port&quot;: &quot;3000&quot;,
 &quot;db_dsn&quot;: &quot;&quot;,
 &quot;github_client_id&quot;: &quot;&quot;,
 &quot;github_client_secret&quot;: &quot;&quot;,
 &quot;db_query_logging&quot;: false
}
</code></pre>
<p>These fields are required:</p>
<ul>
<li><code>app_key</code>: the key to encrypt the data</li>
<li><code>db_dsn</code>: the database connection string</li>
<li><code>github_client_id</code>: the client id for github</li>
<li><code>github_client_secret</code>: the client secret for github</li>
</ul>
<p>You can skip this initialization step by specifying the environment variables:</p>
<ul>
<li><code>APP_KEY</code></li>
<li><code>DB_DSN</code></li>
<li><code>GITHUB_CLIENT_ID</code></li>
<li><code>GITHUB_CLIENT_SECRET</code></li>
</ul>
<p>The full config file is as bellow:</p>
<pre><code class="language-json">{
	&quot;app_env&quot;: &quot;production&quot;,
	&quot;app_key&quot;: &quot;{app_key}&quot;,
	&quot;app_token_key&quot;: &quot;token&quot;,
	&quot;app_port&quot;: &quot;3000&quot;,
	&quot;app_theme&quot;: &quot;default&quot;,
	&quot;db_dsn&quot;: &quot;root:123@tcp(127.0.0.1:3306)/myblog?charset=utf8mb4&amp;collation=utf8mb4_unicode_ci&amp;parseTime=true&quot;,
	&quot;github_client_id&quot;: &quot;{github_client_id}&quot;,
	&quot;github_client_secret&quot;: &quot;{github_client_secret}&quot;,
	&quot;cookie_uuid&quot;: &quot;uuid&quot;,
	&quot;show_tetua_block&quot;: false,
	&quot;db_query_logging&quot;: false,
	&quot;storage&quot;: {
		&quot;default_disk&quot;: &quot;local_public&quot;,
		&quot;disks&quot;: [
			{
				&quot;name&quot;: &quot;local_public&quot;,
				&quot;driver&quot;: &quot;local&quot;,
				&quot;root&quot;: &quot;./public&quot;,
				&quot;base_url&quot;: &quot;http://localhost:3000/storage/&quot;,
				&quot;provider&quot;: &quot;local&quot;,
			}, {
				&quot;name&quot;: &quot;do_public&quot;,
				&quot;driver&quot;: &quot;s3&quot;,
				&quot;root&quot;: &quot;./files&quot;,
				&quot;base_url&quot;: &quot;cdn.myapp.local/files&quot;,
				&quot;provider&quot;: &quot;DigitalOcean&quot;,
				&quot;endpoint&quot;: &quot;sfo3.digitaloceanspaces.com&quot;,
				&quot;region&quot;: &quot;sfo3&quot;,
				&quot;bucket&quot;: &quot;myblock&quot;,
				&quot;access_key_id&quot;: &quot;{access_key_id}&quot;,
				&quot;secret_access_key&quot;: &quot;{secret_access_key}&quot;,
				&quot;acl&quot;: &quot;public-read&quot;
			}
		]
	}
}
</code></pre>
<p>All the environment variables that Tetua use are:</p>
<pre><code>APP_ENV
APP_KEY
APP_TOKEN_KEY
APP_PORT
APP_THEME
DB_DSN
DB_QUERY_LOGGING
GITHUB_CLIENT_ID
GITHUB_CLIENT_SECRET
ROOT_DIR
PUBLIC_DIR
PRIVATE_DIR
COOKIE_UUID
SHOW_TETUA_BLOCK
</code></pre>
<p><strong>Create the Admin account</strong></p>
<pre><code class="language-bash">./tetua setup -u admin -p password
</code></pre>
<p><strong>Run the server</strong></p>
<pre><code class="language-bash">./tetua run
</code></pre>
<p><strong>Result</strong></p>
<pre><code> ┌───────────────────────────────────────────────────┐
 │                  Tetua Community                  │
 │                   Fiber v2.30.0                   │
 │               http://127.0.0.1:3000               │
 │       (bound on host 0.0.0.0 and port 3000)       │
 │                                                   │
 │ Handlers ........... 553  Processes ........... 1 │
 │ Prefork ....... Disabled  PID ............. 30014 │
 └───────────────────────────────────────────────────┘
</code></pre>
<p><img src="https://cdn.tetua.net/files/2022/05/1652344011616476_image.png" alt="" /></p>
]]></content:encoded>
      <author>Ngoc Phuong</author>
      <guid>1</guid>
      <pubDate>Thu, 12 May 2022 06:08:57 +0000</pubDate>
    </item>
  </channel>
</rss>