21 Ağustos 2025 Perşembe

VGA Metin Kipinde Yumuşak Kaydırma İşlemi #1


Merhaba. Bu yazıya, 80'lerin Monospace fontlarla yazılmış bilgisayar kitaplarından çıkmış gibi duran bir başlık seçtim, çünkü bu kez oldukça düşük seviye bir yaklaşımla 80'lerin teknolojisinden bahsedeceğim.

80'lerin Teknolojisi (temsili) [1]

Daha anlaşılabilir bir şekilde, VGA Text modunda smooth scroll nasıl yapılır, kesme (interrupt) kullanmadan doğrudan VGA belleği ve yazmaçları (register) nasıl kullanılır ona değindim. Ama önce VGA'ya kısa giriş yaptım ve bazı yazmaçları anlattım, arada biraz da geyik yaptım. Haliyle yazı düşündüğümden uzun oldu, ben de ikiye böldüm. Smooth scroll'un özü sonraki yazıya kaldı, çünkü kaydırmayla birlikte double buffering'den bahsetmek istiyorum. Bu da uzunca bir konu.

Uyarı: VGA yazmaçlarına, kullanılan donanıma uygun olmayan değerler yazmak donanıma kalıcı zarar verebilir. Burada verilen bilgiler gerçek bir tüplü monitörde denenmemiştir ve bunları kullanmanın riski yalnızca size aittir. Herhangi bir zarar ihtimali varsa, metnin yazarı sizi uyarmakta ve bundan sonrası için sorumluluk kabul etmemektedir.

Orjinal VGA Kart (wikipedia)
Yukarıdaki uyarıyı biraz açıklayayım. Gerçekten de VGA kartını, tüplü (CRT) monitörün desteklemediği bir frekansta çalıştırmak monitöre zarar verebilir [2]. Böyle birşeyle karşılaşmadım ama var olduğunu biliyorum. Öte yandan son CRT monitörümü yaklaşık on yıl önce çöpe attım. Ekran kartım gerçek bir VGA kart değil, yalnızca VGA uyumlu. Dolayısıyla eriştiğim yazmaçlar bile gerçek değil. Kodları DOSBox'ta geliştirip çalıştırdığım ve optimize ettiğim için, gerçek CRT'de muhtemelen timingler kayacak ve waitretrace'leri tekrar ayarlamak gerekecek (waitretrace sonraki yazıda). Bu kodlar 80386'da nasıl çalışırdı bilmiyorum.

Giriş

VGA kartına genellikle int 10h BIOS arayüzünü kullanarak erişiyoruz. Ekran modunu seçmekten, imleci hareket ettirmeye veya büyüklüğünü ayarlamaya kadar herşey bu kesmeyle yapılabiliyor. Kesmenin yaptığı da zaten VGA yazmaçlarına "doğru biçimde" erişmek. VGA BIOS, gerekirse int 10h rutinlerini karta uygun şekilde değiştirebiliyor.

Int 10h'un ciddi bir yavaşlık getirdiğini düşünmüyorum (putpixel vb. işlemler hariç), hatta kesmeyi tercih etmenin bir avantajı da kodda karmaşıklıktan kaçınmak. VGA'nın çok sayıda yazmacı var [3]*. Bunların bazısının işlevini anlamak için temel CRT bilgisi gerekiyor. Diğer yandan bu yazı için DOSBox kullandığımı belirttim. DOSBox bir çok VGA registerini doğru emüle edebilse de bazı (standart dışı) efektleri düzgün gösteremiyor. Dolayısıyla bazı DOS oyunları düzgün çalışmıyor (konfigürasyon hatalarını bir kenara). Yine de DOSBox'un hakkını teslim etmek gerek, Vmware veya VirtualBox'la karşılaştırıldığında daha uyumlu (evet elmayla armudu karşılaştırdım: biri DOS'a özel, diğerleri genel sanallaştırma).

* Adı geçen kaynakta 300+ yazmaçtan bahsediliyor ama 100 tane bile dökümante edilmemiş. Muhtemelen, farklı üreticilerin kendi ekledikleri standart dışı yazmaçlar sayılıyor. Standart yazmaçlar yaklaşık 60 tane, ama bu da az değil.


VGA Yazmaçları ve Karta Erişmek

Yukarıda da yazdığım gibi bir çok VGA yazmacı var. Şu linkte yazmaçlar altı grupta toplanıyor. Bu yazıda ben çoğunlukla CRT denetleyici (CRT controller - CRTC) yazmaçlarına erişeceğim. Gruplar kabaca erişimde kullanılan port numaralarına göre oluşturuluyor.

Bu yazmaçlara, kabaca 6 çift donanım portu üzerinden erişiliyor. Yani altmış yazmacın her birine bir port atanmamış. [3]'te portların bir listesi var. Genel olarak erişim, 0x3DX'e yazmaç index numarasını girdikten sonra, 0x3DX+1'den yazmaçtaki değeri okumak veya değere yazmak şeklinde. 0x3D0'ın bir istisnası var ama ona değinmeyeceğim. Yazının ileriki bölümlerinde bu yazmaçlarla ilgili örnek göstereceğim.

Burada tüm yazmaçlara yer verip yazıyı bir referans kitabı haline getirmek istemedim. O yüzden sadece ilgilendiğim kısma odaklanacağım. Örneğin: CRTC'ye, 0x3D4 ve 0x3D5 portları üzerinden ulaşılıyor. 0x3D4 yazmaç seçme, 0x3D5 ise veri okuma ve yazma portu [5].


Cursor Start Register (Index 0Ah)
76543210


CDCursor Scan Line Start


Şimdi [6]'daki imleci devre dışı bırakma kodunu ele alalım:

void disable_cursor()
{
    outb(0x3D4, 0x0A);
    outb(0x3D5, 0x20);
}

Önce 0x3D4 portuna 0xA yazmacına erişeceğimizi bildiriyoruz. Ardından bu yazmaca 0x3D5 portu aracılığıyla 0x20 değerini yazıyoruz. Bu değer 0xA'nın 5. bitini yani Cursor Disable bitini set ediyor [5]. Oldukça basit.

Cursor Scan Line Start, imlecin hangi pixelden başlayacağını gösteriyor. Standart 80x25 karakter ekran modunda (Mod 3), her karakter ve imlecin kendisi 8x16 pixelden oluşan bir görsel. Bu görseller değiştirilerek DOS'ta font yüklenebilir. Aşağıda, DOS için bir font düzenleme programından aldığım ekran görüntüsünde, örnek bir karakter yakından görülüyor. Font tablosu VGA BIOS'ta bulunuyor (meraklısına Int 10h / 1130h) ve özel fontlar bu alana geçici olarak yazılıyor (bilgisayar yeniden başlatılana kadar). Fontlar başlı başına bir yazı konusu, daha fazla detaylandırmayacağım.

Konuya geri dönersem, edit ortamında imleç 14. pixel satırından başlayıp 15.'de biter, ama edit'te Insert'e basıldığında 0. pixel satırından başlar. İşte bu etkiyi veren birinci öğe Cursor Start Register'in düşük anlamlı beş biti, ve ikinci öğe 0xB numaralı Cursor End Register'dir, daha doğrusu bunun düşük anlamlı beş biti:


 Cursor End Register (Index 0Bh)
76543210

Cursor SkewCursor Scan Line End


Bu yazmaç imlecin alt pixel satırını tutar. Peki eğer bir karakter 16 pixel uzunluğundaysa neden 5 bit? VGA, metin modunda aslında 32 pixele kadar olan karakterleri destekler [6]. Örn. VGA font tablosu 8 KB uzunluktadır: Karakterlerin sayısı (256) * yüksekliği (32 px) * genişliği (8 px) / bit per byte (8). Bu yüzden imleç için de yazmaçta 5 bit yer ayrılmıştır, ancak dördüncü bitin hiçbir metin modunda anlamı yoktur. Cursor Skew bitleri EGA uyumluluğu için bırakılmıştır, yine VGA'da bir anlamı yoktur.

Anlaşılması kolay başka bir yazmaç çifti de Cursor Location High (0xE) ve Cursor Location Low (0xF) yazmaçlarıdır. İmlecin ekrandaki doğrusal konum bilgisini tutarlar. Bu değer, ekranın karakter çözünürlüğüne (bizim örneğimizde 80) kalanlı bölündüğünde bölüm, imlecin y-eksenindeki; ve kalan, imlecin x-eksenindeki yerini verir. Tersi ifadeyle D = Y * 80 + X. Bu yazmaçlar byte uzunlukta olduklarından D'nin yüksek anlamlı byte'ı 0xE'ye, düşük anlamlı byte'ı 0xF'e yazılır.

Cursor Location High Register (Index 0Eh)
76543210
Cursor Location High


Cursor Location Low Register (Index 0Fh)
76543210
Cursor Location Low


80'lere Geri Dönüş: QBasic

Şimdi bu iki çift yazmaçla küçük bir demo yapacağım ve bunun için ilginç bir şekilde QBasic kullanacağım. Aslında ben de pek çok kişi gibi programlamayı Basic'le öğrendim. Biraz C64 Basic, sonrasında GW-Basic (o zamanın TRT'si sağolsun, TRT4 Açıköğretim Bilgisayar derslerı) ve son olarak QBasic. Ve iddia ediyorum ki 80'lerde doğup 90'larda bilgisayarı olan herkes aşağıdaki IDE'yi en az bir kere görmüştür. Ben 90'larda .bat dosyaların yetersiz kaldığı durumlarda betiklerimi QB ile yazardım. Sonrasında QBasic'in hızı bir çok şey için yetersiz gelince, bu beni C ve Assembly öğrenmeye itmişti. -Arada kısa bir Pascal dönemim de oldu.- Bu arada QBasic bir yorumlayıcıydı (interpreter) ve .exe dosya üretememesi benim için başka bir eksiklikti. C ile yakın zamanlarda Quick Basic v4.5 ile tanışsam da, C'nin açtığı ufuk bambaşkaydı. Ayrıca o zamanlarda Quick Basic 4.5 IDE'yi bulmak da -en azından benim için- oldukça zordu.

Yalnız ne QBasic ne Quick Basic (kısaca QB) yetersiz programlar değildi. C ile yapabildiğim herşeyi QB ile de yapabiliyordum. Bulabildiğim eski kodlarıma baktığımda, fare için QB - Int33h arayüzü ve disk işlemleri için Int13h arayüzü yazmışım. Yine başkalarının yazdığı inanılmaz kodlar var. Ama yavaşlığı bir yana, QB'deki programlama mantığı da biraz "başkaydı", ve programlamanın gitmekte olduğu yerden farklıydı. Visual Basic'le de şansımı denedim ama o ara Windows'un genel olarak bana göre olmadığını fark ettim. 2000'lerin başında görsel programlamada Win32 Assembly ile uğraştım, ama Win32API bana ağır gelmişti.

Ve neredeyse on yıldır blog yazıp, bu yazılarda bir sürü dilde örnek program yazmama rağmen, tek bir QB örneği vermediğimi fark ettim. Oysa bu tür basit kod parçaları için QB bence daha kolay, çünkü ne Assembly kadar çok satıra gerek var, ne de C'deki gibi include ekle, type cast'lere dikkat et, buffer'ı kontrol et gibi konular yok. Kısacası bu kadar retrospective yeter. Bu yazıdaki örneği QB ile veriyorum:


DECLARE SUB ENABLECURSOR (CURSTART%, CUREND%)
DECLARE SUB DISABLECURSOR ()
DECLARE SUB MOVECURSOR (CURSORX%, CURSORY%)

FOR X% = 0 TO 15
  FOR Y% = X% TO 15
    CALL ENABLECURSOR(X%, Y%)
    SLEEP 1
    CALL DISABLECURSOR
    SLEEP 1
  NEXT Y%
NEXT X%

CALL ENABLECURSOR(0, 15)

FOR Y% = 0 TO 10
  FOR X% = 0 TO 10
    CALL MOVECURSOR(X%, Y%)
    SLEEP 1
  NEXT X%
NEXT Y%

SUB DISABLECURSOR
    OUT &H3D4, &HA
    OUT &H3D5, &H20
END SUB

SUB ENABLECURSOR (CURSTART%, CUREND%)
    OUT &H3D4, &HA
    CS1% = INP(&H3D5)
    OUT &H3D5, (CS1% AND &HC0) OR CURSTART%

    OUT &H3D4, &HB
    CE1% = INP(&H3D5)
    OUT &H3D5, (CE1% AND &HE0) OR CUREND%
END SUB

SUB MOVECURSOR (CURSORX%, CURSORY%)
    POSITION% = CURSORY% * 80 + CURSORX%

    OUT &H3D4, &HF
    OUT &H3D5, POSITION% AND 255
    OUT &H3D4, &HE
    OUT &H3D5, POSITION% \ 256
END SUB


Kod biraz uzun ama genel olarak [4]'teki kodları içeriyor. İlk bölümde imlecin alabileceği kombinasyonları bir for döngüsünde oluşturdum. Parametreler ENABLECURSOR alt programında ilgili yazmaçlara gönderiliyor. Bu arada döngü içerisinde bir saniyelik bekleme (SLEEP) CTRL tuşu basılı tutularak geçilebilir.

İlk for döngüsünden sonra rahat görülebilmesi için imleci büyütüp, MOVECURSOR alt programıyla ekranın 10 x 10'luk bölümünde hareket ettirdim. MOVECURSOR'de ekranın 80 karakter genişlikte olduğunu varsayıp, (X, Y) koordinatlarından imlecin doğrusal konumunu hesaplattım.

Bir sonraki yazıda başta yumuşak kaydırma için gereken yazmaçlar olmak üzere diğer VGA yazmaçlarına değineceğim ve QB ile başka örnekler vereceğim. Ancak kaydırma işlemi yüksek hız gerektirdiği için onu Assembly + C ile yazdım, ve yazının başlarında değineceğimi söylediğim waitretrace fonksiyonunu kullandım.



[1]: DEC PDP8 Family User's Guide TSS/8 (1970). Link
[2]: https://retrocomputing.stackexc....damage-my-vga-card-by-programming-it-in-assembly-throu
[3]: http://wiki.osdev.org/VGA_Hardware
[4]: http://wiki.osdev.org/Text_Mode_Cursor
[5]: http://www.osdever.net/FreeVGA/vga/crtcreg.htm
[6]: https://en.wikipedia.org/wiki/VGA_text_mode#Fonts

10 Temmuz 2025 Perşembe

Python ile Görsellere Metin Eklemek

Merhaba. Bu yazıda ihtiyaca yönelik bir problemin çözümüne değindim. Problem, bir dizindeki görsellere toplu bir şekilde yazı eklemek. Örneğin bir telif notu veya adres bilgisi gibi. Sanıyorum bu, GIMP'te bir şablon (stencil) oluşturularak çözülebilir. Peki eklenecek metin sabit olmadığı durumda? Benim sorunum görsellere sıra numarası eklemekti. Her fotoğrafın köşesine birden başlayan ardışık sayılar... Bu durumda, şablon çözüm olmuyor. Ayrıca bu GIMP'le tek tek yapılabiliyorsa bile yüze yakın görsel için pratik değil. En pratik çözüm bunun için basit bir script yazmak. Bu arada, bu numaralandırmayı dosya adıyla yapabilirdim, ama dosya adındaki zaman damgasını korumak istediğim için buna dokunmak istemedim. Üstelik bu görselleri bir web sayfasında kullanmak isteseydim, dosya adları görünmüyor olacaktı. Son olarak bu script ile istenirse dosya adlarından, görselin kenarına 90'larda fotoğraf makinalarının yaptığına benzer şekilde fotoğrafın ne zaman çekildiği yazılabilir. 

Script deyince akl(ım)a ilk gelen bash maalesef bu iş için en iyi araç değil. Bildiğim kadarıyla bash için yazılmış bir görsel kütüphanesi yok. Yapılmaz değil elbette yapılır, ama nasıl ki balyoz varken duvar yıkmak için çekiç kullanılmıyorsa, her iş için ona uygun aracı seçmek çözümün ilk adımı. Python sever okurlar kızacaktır ama bash'tan sonra ikinci en iyi script dili python'un bu iş için Python Imaging Library (PIL) adında uygun bir kütüphanesi var. Ben kendi scriptimde PIL'in Pillow adındaki fork'unu kullandım. Kütüphane pip install pillow komutuyla kolayca yükleniyor. 

Yazıyı kısa tutmak için kodu yine github hesabıma yükledim. Kısa bir script ve içinde sadece ufak numaralar var. Öncelikle her normal python script'i gibi başta import'lar var. PIL kütüphanesindeki Image modülü görsellerle ilgili fonksiyonları içeriyor. ImageDraw, görsellerle ilgili basit 2B efektler, benim kodumda görseli döndürmek için gerekli ve son olarak ImageFont font ve diğer metin efekleri için. 


Exif Verileri ve Oryantasyon

Bu projedeki birinci zorluk, görsellerin bilgisayarda, telefonda çekildikleri şekilde görüntülenememeleri. Örn. aşağıda cep telefonumla çektiğim görseli ele alalım:


Yukarıda, tarayıcıda dikey formatta görünüyor. Kendi bilgisayarımda Gwenview ile açtığımda yine dikey formatta görünüyor. Ancak aşağıdaki kodla açtığımda

>>> from PIL import Image
>>> img = Image.open("image.jpg")
>>> img.show()

görsel yatay formatta görünüyor. 

ve GIMP'le açtığımda garip bir pencere görselin "Exif orientation metadata" içerdiğini ve bunu çevirmek isteyip istemediğimi soruyor. Peki neden?

Cep telefonunu yatay* tutup fotoğraf çekerken, telefon fotoğrafa herhangi bir döndürme işlemi uygulamaz. Cep telefonu veya dijital fotoğraf makinasıyla çekilmiş fotoğrafları python ile yukarıdaki gibi açtığımda, dikey açıyla çekilmemiş olan fotoğraflar bu yüzden çekildikleri açıda ekrana basılırlar. Kamera fotoğrafın çekildiği oryantasyonu da birlikte kaydedip, görüntüleme sırasında fotoları çevirir. Yönelim (oryantasyon) bilgisi kaydedilmemiş olsaydı, fotoğrafı düzgün görmek için her seferinde bizim telefonu ilk çekildiği açıya çevirmemiz gerekirdi. 

*: Bazı kameralarda dikey pozisyon default olup, yatay pozisyonun yönelimini kaydederken, bazılarında tam tersidir. 

Dolayısıyla bir görsel dosyasının içinde sadece piksellere ait bilgiler olmadığı yukarıdaki ifadeden de anlaşılabilir. Dosyada görsele ait metadatanın saklandığı Exif adlı bir alan vardır ve bugün tüm görsel formatları ve kameralar Exif'i destekler. Wikipedia'da Exif'te saklanan verilere ait örnek bir tablo var. Buna göre başlıca alanlar, kameranın marka ve modeli, görselin yönelimi, çekildiği gün ve saat, çözünürlük vb. Örn. çekildiği gün ve saat bilgisinin fotoğrafa gömülmesi, dosya adı 20230809_143341.jpg formatında olmasa bile fotoğrafın ne zaman çekildiğinin bulunmasına ve tarihe göre sıralabilmesine olanak sağlar. Öte yandan bazı telefonlar GPS'ten aldığı koordinat verisini Exif'e gömerek fotoğrafın nerede çekildiğini açık ederler. Bazı ortamlar böylece fotoğrafın çekildiği yeri paylaşılırken otomatik etiketleyebilir. Bunlar kişisel bilgilerin gizliliği konusunda hassas olanların tüylerini diken diken edecek durumlar. Bir söylentiye göre Ukraynalılar, Rus askerlerinden online foto isteyip bundan elde ettikleri koordinatlara saldırılar düzenlediler. 

Linux'ta exiftool adında bir araçla bu bilgiler görüntülenebilir (exiftool -list <dosyaadi>), değiştirilebilir veya tamamen silinebilir (exiftool -all= <dosyaadi>). Yukarıdaki örnek görselin tüm Exif verisi silinmiş haliyle orjinali arasında 77 KB civarı bir fark var. 

Exif konusunda gereğinden uzun bir açıklama yaptıktan sonra, tekrar Exif oryantasyon bilgisine geri döneyim. Pillow kütüphanesinde Exif'te saklanan veriyi okumak için Image.getexif() fonksiyonu var [1]. Bunun çıktısını ekrana yazdırdığımda (kodun içinde 22., 23. ve 24. satırlar, comment out edilmiş) dizindeki .jpg dosyaların 'Orientation' bilgisini görüyorum. [1]'de de belirtildiği gibi 2, 7, 4 ve 5 benim görsellerde yoktu. Keza 8 de bulunmadığı için bunu kendi kodumda uygulamadım, ancak bunu uygulamak gayet kolay. 1 değeri için if yapısında else'i kullandım (satır 31), bu nedenle 8 ve diğer değerlerde görsel döndürülmüyor. 

Döndürme için Pillow'da Image.rotate() fonksiyonu var [2]. Bunu kullanarak Orientation=3 için görseli 90 derece ve Orientation=6 için görseli 270 derece çevirdim. Burada 27. satır için bir parantez açmak gerek: Eğer Exif'te yönelim alanı bulunmuyorsa veya görselde Exif bilgisi yoksa kod hata verecektir. Bu nedenle getexif fonksiyonundan dönen değeri kontrol edip herşey tamamsa döndürmek daha doğru. Bendeki verilerin hepsinde Exif bulunduğu için sorun yaşamadım, bu nedenle bu kısmı hızlı ve çabuk şekilde halletmeyi tercih ettim.

Buraya kadar görselin yönelimini düzeltmiş olduk. Ana problemin çözümünde [3]'ten yararlandım. Burada nasıl yazı ekleneceği basitçe gösterilmiş, ben yalnızca kendi problemime göre parametreleri düzenledim. Satır 35'te yazı eklenmek üzere bir ImageDraw nesnesi oluşturuluyor. Sonraki satırda ImageFont.truetype() fonksiyonuyla bir font nesnesi oluşturuluyor. [3]'te kullanılan font benim makinamda olmadığından (ve fonksiyondan dönen değeri hata için kontrol etmediğimden), ben /usr/share/fonts/ altındaki fontlardan birini seçtim. İkinci parametre font büyüklüğü (punto). Bunu deneme yanılma yoluyla buldum. Bendeki görseller görece büyüktü (8 MP) bu nedenle 128 ancak görülebilir bir yazı üretebildi. Ondan sonraki satırda sırasıyla, görselin verilen koordinatına (25, 25 - sol üst), sirano değişkenini, bir önceki satırla oluşturduğum fontla ve kırmızı renkle ekledim. Bu adımda, yukarıda bahsettiğim, dosyanın adından veya Exif etiketinden fotoğrafın ne zaman çekildiği bilgisi alınarak yazdırılabilirdi. Aşağıda örnek bir numaralandırılmış fotoğraf görülüyor:


Son olarak, 40. satırdaki yorum kaldırılarak görsel ekranda gösterilebilir ve/veya 43. satırdaki yorum kaldırılarak "_enum" sonekiyle kaydedilebilir. Ben bu scripti yazarken yüze yakın görselin olduğu bir dizinde çalıştırdığım için her seferinde yüz fotoyu göstertmek veya kaydetmek istemediğimden o satırları comment out etmiştim. 


Not: Alternatif olarak bunu OpenCV'de cv2.putText() fonksiyonuyla yapabilmek de mümkün ama bu başka bir yazıya kalsın. 

 

[1]: https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
[2]: https://note.nkmk.me/en/python-pillow-rotate/
[3]: https://www.geeksforgeeks.org/python/adding-text-on-image-using-python-pil/ 

5 Ocak 2025 Pazar

Amazon S3 Objelerinde ETag Hesplaması


Merhaba. Bu yazıda S3'e yüklenen dosyaların ETag adı verilen hash'lerinin nasıl hesaplandıklarından bahsedeceğim. ETag genel olarak, bir S3 bucket'ına yüklenen her dosya için hesaplanan MD5 hash'ten başka birşey değil. S3'teki dosyaların gerçekte bizim anladığımız anlamda dosya olmadıklarını biliyoruz. S3, "Object Storage" olarak alınıyor ve dosyalar da doğru terminolojiyle obje olarak saklanıyor. 

Belli bir dosya büyüklüğünden sonra, aws s3 cp veya aws s3 sync ile yapılan yüklemeler, (muhtemelen) daha kolay saklanabilmesi için otomatik olarak eş büyüklükte parçalara bölünür. Buna multipart objeler denir. Peki ne kadar bir büyüklük? Genel geçer bir ölçü olmasa da benim dosyalarım şu anda 8 ila 16M arası parçalar olarak saklanıyor. Yazıyı hazırlarken kullandığım kaynakta, 5G'ye kadar olan dosyaların parçalanmadıklarından bahsedilmiş [1][2], ancak benim gözlemime göre bu artık doğru değil ama bu değer çok da önemli değil.

Eğer bir dosya bu eşikten küçükse, tek parça olarak saklanır ve objenin ETag'i MD5 hash'ine eşit. Buraya kadar bir sorun yok. Eğer dosya bu eşikten büyükse multipart obje olarak saklanınca işler biraz karışıyor. Bir objenin multipart olup olmadığı ETag'ına bakarak kolayca anlaşılabilir. Normal bir MD5 hash yalnızca hexadecimal basamaklardan oluşur. Dolayısıyla tire işareti ( - ) MD5 hash'e ait değildir. Eğer S3'teki bir dosyanın ETag'ında tire işareti varsa, bu multipart bir objedir ve dosyanın kaç parçaya bölündüğü tireden sonra gelen kısımdadır. Bunların hepsine ait somut örnekleri yazının ilerleyen kısmında vereceğim. 

Multipart objelerde ETag hesaplaması şöyle işliyor: Her bir parça ayrı ayrı MD5'le hash'leniyor, çıkan hash'ler uç uca eklenip tekrar hash'leniyor. Bu ETag'ın tireden önceki kısmı. Parça sayısı basitçe tireden sonra en sonra ekleniyor [3].

Ben bilgisayarlarımın disklerini düzenli olarak Clonezilla ile yedekliyorum. Yedekleri önce harici diske alıp, bunları S3'e kopyalıyorum. Harici diskte en yeni kopya duruyor, son üç kopya S3'te. Geriye doğru (FAT32) uyumluluk nedeniyle, yedekleri 4G'lik parçalara bölüyorum (her ne kadar yedeği FAT32 ortama almasam da). Zaten ETag karşılaştırma ihtiyacı S3'teki kopyaları doğrulamak istememden çıktı.

Bu noktada aws komut satırı arabiriminin yüklü ve ayarlı olduğunu varsayıyorum. Ayarlar .aws/config dosyasından yapılıyor ancak yazıyı uzatmamak için buna değinmeyeceğim. Önce küçük dosya örneğini ele alalım:

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/Info-lshw.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 40960,
    "ETag": "\"fe78f69cb9d41a23ba23b4783e542a7b\"",
    "ContentType": "text/plain",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Önceden belirttiğim gibi, bu multipart obje değil. Haliyle MD5 hash'i yani ETag'ı basitçe bulunabilir. Aşağıda büyük dosya örneği var: 

$ aws s3api head-object --bucket mybucket --key image_backup/2024-12-01-13-img/sda5.ntfs-ptcl-img.xz.ac
{
    "AcceptRanges": "bytes",
    "LastModified": "2024-12-03T17:00:58+00:00",
    "ContentLength": 4096008192,
    "ETag": "\"360f5e8babf8cd28673eaafd32eb405f-489\"",
    "ContentType": "application/vnd.nokia.n-gage.ac+xml",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}

Bu 4096 MB'lık bir dosya ve ETag'dan görüleceği gibi 489 parçadan oluşuyor. Burada önemli olan parçaların büyüklüklerini bulmak. ContentLength, 489'a bölününce 8M'ye çok yakın bir değer bulunuyor. Buradan aslında dosyanın 8M'lik parçalara bölündüğünü varsayabilirim ama bir programda kullanmak için bunun kesin değerini bulmak gerek. Bunun için aynı komuta --part-number parametresini ekleyip tek bir parçayı inceleyeceğim. Dosyalar sabit büyüklükte parçalandıklarından yalnızca en son parçanın boyutu farklı, ancak her parça için ETag değeri aynı. Başka bir deyişle --part-number her parçanın ayrı ayrı MD5 hash'ini vermiyor.

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 16777216,
    "ETag": "\"aba379cb0d00f21f53da5136fc5b0366-299\"",
    "ContentType": "audio/aac",
    "ServerSideEncryption": "AES256",
    "Metadata": {},
    "PartsCount": 299
}

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 299
{
    "AcceptRanges": "bytes",
    "LastModified": "2023-10-15T18:28:31+00:00",
    "ContentLength": 401408,
    "ETag": "\"aba379cb0d00f21f53da5136fc5b0366-299\"",
    "ContentType": "audio/aac",
    "ServerSideEncryption": "AES256",
    "Metadata": {},
    "PartsCount": 299
}

Bu arada resmi AWS dökümantasyonuna göre (Aralık 2024 itibariyle) [4] default parça büyüklüğü 8 MB ancak yukarıda görüldüğü üzere Ekim 2023'te bir dosya 16 MB'lik parçalarla yüklenmiş. Dolayısıyla bu değeri sabit kabul etmek yerine, ContentLength alanından almak daha mantıklı. Görünüşe göre Amazon'dakiler canları sıkıldıkça default'u değiştiriyorlar. Bu arada aws komutu json çıktı üretiyor. bash script'le çalışırken, çıktıyı grep yerine jq ile parse etmek daha şık sonuç veriyor:

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1 | jq -r '.ETag'
"aba379cb0d00f21f53da5136fc5b0366-299"

$ aws s3api head-object --bucket mybucket --key image_backup/2023-10-15-10-img/sda5.ntfs-ptcl-img.gz.aac --part-number 1 | jq -r '.ContentLength'
16777216

Ben, aldığım yedekteki tüm dosyaları tek tek karşılaştırmak için bir script hazırladım. Biraz uzun olduğu için buradan paylaşmayacağım, repo linkiyle ulaşılabilir. Script, kullanıcıdan basitçe bucket adını ve yedeklerin olduğu dizinin adını alıyor. Ben yedekleri image_backup adında bir dizinde, <YYYY-MM-DD-HH-img> formatlı alt dizinlerde tutuyorum, bu kısım (satır 12) ihtiyaca göre değiştirilebilir. Parça sayısı birse, doğrudan md5 alınıyor (satır 26). Birden fazla parça varsa, bu parçalar dd ile bölünüyor (satır 36), hepsinin ayrı ayrı hash'leri geçici bir dosyaya yazılıyor. Parçalar bittiği zaman oluşan dosyanın tekrar hash'i alınıp dosya siliniyor (satır 41-42). Dosyanın geri kalan kısmı bash string işlemleriyle hash'ler karşılaştırılıp aynı ise OK farklı ise FAIL yazdırılıyor.


[1]: https://stackoverflow.com/questions/45421156
[2]: https://stackoverflow.com/questions/6591047
[3]: https://stackoverflow.com/questions/12186993
[4]: https://docs.aws.amazon.com/cli/latest/topic/s3-config.html#multipart-chunksize