概要
表題の通り、昨日私が投稿いたしました
Visual Studio 2022のC#やVisual Basicを使用し、Android12やWindows11環境下において任意のファイルを直接tarファイルにバイナリを叩いて強引にアーカイブする。コードのサンプルです。
https://qiita.com/oyk3865b/items/942f1c5ef7c80f4f5cae
という無駄に長いタイトルのページを作成後に
せっかくなので.tarまでアーカイブしたのなら、
.tar.gzまで作ったらどうなんだ?という
ご期待に添えての記事でございます。
また、前回の記事で併記した、
・WindowsでC#環境の場合のコード
・WindowsでVisual Basic環境の場合のコード
でも、やっていることはほとんど同じなので、
この記事では省略させていただきます。
尚、特段、NuGetなどで他のプラグインや
別途外部dllを必要としないようにしていますが、
その反面、自身への備忘録として荒く、
とても強引な内容なので、そのままのコード流用はご注意ください。
要は、下記の参考サイト様の情報を元に
tarのバイナリを内部構造のルールに従い、直接叩いています。
参考サイト様
◎『tar の構造 - インターネットの孤島』
http://www.redout.net/data/tar.html
◎『tar -- format of tar archives』
http://www.mkssoftware.com/docs/man4/tar.4.asp
その他コード中のURLのサイト様かより
コードの引用をしたり、参考にさせてもらった部分がございます。
この場に手厚く御礼申し上げます。
注意事項
※私は素人ですし
今回のコードは、あくまで最低限の動作する部分にとどめていますので、汚いです。
例外処理や、解放など至らぬ点が、多々ございます。ご了承ください。
もし実際に参考にされる際は、必要個所を必ず訂正・加筆してからにしてください。
※今回は特に内部でのファイル名の扱いや日時の扱いが
すごく適当です。
※動作は沢山メモリを使うので重いです。
その辺は度外視しています。
本題
Android C#環境の場合のコード
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Systems;
using Android.Views;
using Android.Widget;
using Java.Nio;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using static Android.Provider.ContactsContract.CommonDataKinds;
using static Java.Text.Normalizer;
using static System.Net.Mime.MediaTypeNames;
namespace App1
{
class clsTarArchive
{
//出力用のバイナリ格納用
public List<byte> ary_tar_file;
public string filesize_str;
public async System.Threading.Tasks.Task Convert_File_to_tar_gz(
Android.Net.Uri Output_Path, Android.Net.Uri[] Archive_filepaths,
Activity ac)
{ //指定したファイル達をTarにアーカイブするところ
//安全装置
string err_string = "0";
if (Archive_filepaths.Length == 0) { return; }
try {
//■ファイルを作成して書き込む
string temp_name = System.IO.Path.GetTempFileName();
using (System.IO.FileStream fs = new System.IO.FileStream(temp_name,
System.IO.FileMode.Create,
System.IO.FileAccess.Write))
{
err_string = "1";
int content_count = Archive_filepaths.Length;
for (int i = 0; i < content_count; i += 1)
{ //各ファイルをループ
//出力TARバイナリの初期化
ary_tar_file = new System.Collections.Generic.List<byte> { };
ary_tar_file.Clear();
//一時格納用配列
System.Collections.Generic.List<byte> tar_write_binary = new System.Collections.Generic.List<byte> { };
tar_write_binary.Clear();
//■ヘッダー(512Bytes)の作成------------ここから-----------------
//http://www.redout.net/data/tar.html#ascii
//まずは、ファイル名を格納
//Uriからファイル名を取得
string filename = await getRealPathFromURI(ac, Archive_filepaths[i]);
tar_write_binary.AddRange(System.Text.Encoding.UTF8.GetBytes(filename));
//何かでの経過報告用文字列
string v = "Open File : " + filename + "Size : " + filesize_str;
err_string = "2";
retry_line:
if (tar_write_binary.Count >= 100)
{ //99文字以内にする
//→今回は、左から消して徐々に短くして対応する。
tar_write_binary.Clear();
filename = filename.Substring(filename.Length - 1);
tar_write_binary.AddRange(System.Text.Encoding.UTF8.GetBytes(filename));
goto retry_line;
}
//バイナリに書き込む予約
err_string = "2.5";
ary_tar_file.AddRange(tar_write_binary.ToArray());
tar_write_binary.Clear();
err_string = "3";
//→ファイル名ヘッダーを、100バイトであわせる
while (true) {
if (ary_tar_file.Count >= 100) { break; }
ary_tar_file.Add((byte)(0)); //0で埋める
}
v = "mode File : " + filename + "Size : " + filesize_str;
//◆次に、mode→uid→gidの並びだが、面倒なので。
//固定値にする。解析が不十分orz
//◎mode
err_string = "4";
ary_tar_file.AddRange(System.Text.Encoding.ASCII.GetBytes(
"0100666"));
ary_tar_file.Add((byte)(0)); //0で区切る
//◎uid(所有者のユーザIDです。)
err_string = "5";
//http://www.c-lang.net/stat/index.html
ary_tar_file.AddRange(System.Text.Encoding.ASCII.GetBytes(
"0000002"));
ary_tar_file.Add((byte)(0)); //0で区切る
//◎gid(所有者のグループIDです。)
err_string = "6";
ary_tar_file.AddRange(System.Text.Encoding.ASCII.GetBytes(
"0000002"));
ary_tar_file.Add((byte)(0)); //0で区切る
//◆ファイルサイズを、8進数で示した文字列で埋め込む。
err_string = "7";
int filesize = 0;
if(int.TryParse(filesize_str, out filesize)) {
//10進数→8進数
filesize_str = Convert.ToString(filesize, 8);
//ファイルサイズ箇所は、11バイト分用意されている。
//それを超えるファイルは、扱えないので、そのファイルは飛ばす
if (filesize_str.Length > 11) { break; }
v = "8size File : " + filename + "Size : " + filesize_str;
}
//空白桁は"0"で埋める
while (true)
{ //11バイトまで"0"文字で埋める
if (filesize_str.Length >= 11) { break; }
filesize_str = "0" + filesize_str;
}
ary_tar_file.AddRange(System.Text.Encoding.ASCII.GetBytes(filesize_str)); //バイナリに書き込む
tar_write_binary.Clear();
ary_tar_file.Add((byte)(0)); //0で区切る
filesize_str = "";
//◆アーカイブされた時点でのファイルの最終更新時刻
//★今回は手抜きで現在時間とする超強引コード
//(※※※こんなコードで、大丈夫か?※※※)
err_string = "8";
DateTime dtBirth3 = DateTime.Parse(DateTime.Now.Year.ToString() + "/" + DateTime.Now.Month.ToString() +
"/" + DateTime.Now.Day.ToString() + " " + DateTime.Now.Hour.ToString() + ":" + DateTime.Now.Minute.ToString());
//秒を含まないシリアル値に変換する
long Last_W_Time = (long)(dtBirth3.ToBinary() / Math.Pow(10, 8));
//今回は、6213562917を、引いて、さらに10倍して
//擬似的に”stat() 関数で得られる最終更新時刻”の近似を得る。
Last_W_Time = (Last_W_Time - 6213562917) * 10;
//11桁の8進数に変換
filesize_str = Convert.ToString(Last_W_Time, 8);
//空白桁は"0"で埋める
while (true)
{ //11バイトまで"0"文字で埋める
if (filesize_str.Length >= 11) { break; }
filesize_str = "0" + filesize_str; //空白桁に"0"を付ける。
}
if (filesize_str.Length != 11) {
//変換に失敗したら、適当に、
filesize_str = "12211506030";// 2013/09/04 10:25とする。
}
v = "date File : " + filename + "Date : " + filesize_str;
tar_write_binary.AddRange(
System.Text.Encoding.ASCII.GetBytes(filesize_str)); //バイト配列に変換
ary_tar_file.AddRange(tar_write_binary.ToArray()); //バイナリに書き込む
tar_write_binary.Clear();
ary_tar_file.Add((byte)(0)); //0で区切る
//◆チェックサム→後で計算して入れるので、今は、8バイト適当に入れる。
int chksum_pos = ary_tar_file.Count; //開始位置を予め覚えておく
for (int k = 0; k < 8; k += 1) {
ary_tar_file.Add((byte)(0)); //ちなみに、0開始だと、147~154の位置
}
//◆typeflag/アーカイブの種類を決定する。
//とりあえず、今は、0(通常のファイル)にしておく
ary_tar_file.AddRange(System.Text.Encoding.ASCII.GetBytes("0"));
//◆linknameで100バイトの余白
for (int k = 0; k < 100; k += 1)
{
ary_tar_file.Add((byte)(0));
}
//◆magic フィールドと、version
v = "magic File : " + filename + "Date : " + filesize_str;
ary_tar_file.AddRange(System.Text.Encoding.ASCII.GetBytes(
"ustar "));
ary_tar_file.Add((byte)(0)); //0で区切る
//◆uname 32バイト
tar_write_binary.AddRange(
System.Text.Encoding.ASCII.GetBytes(
"tagesp"));
while (true)
{ //11バイトまで"0"文字で埋める
if (tar_write_binary.Count >= 32) { break; }
tar_write_binary.Add((byte)(0)); //0で埋める
}
ary_tar_file.AddRange(tar_write_binary); //バイナリに書き込む
tar_write_binary.Clear();
//◆gname以降のヘッダー全て 215バイト
tar_write_binary.AddRange(
System.Text.Encoding.ASCII.GetBytes(
"tagesp"));
while (true)
{ //11バイトまで"0"文字で埋める
if (tar_write_binary.Count >= 215) { break; }
tar_write_binary.Add((byte)(0)); //0で埋める
}
ary_tar_file.AddRange(tar_write_binary.ToArray()); //バイナリに書き込む
tar_write_binary.Clear();
//◆◇◆ここが本番。チェックサムの格納。◆◇◆
Int32 chk_sum = 0;
int idx = 0;
while (true) {
if (idx >= ary_tar_file.Count) { break; }
if (idx >= 147 && idx <= 154) {
//チェックサム領域は、計算しない
} else {
//ヘッダーブロックの全バイトの和を 8 進数で表した値
chk_sum += Convert.ToInt32(ary_tar_file[idx]);
}
idx++;
}
//8進数に変換
//+256は、チェックサムの空白分
filesize_str = Convert.ToString(chk_sum + 256, 8);
//空白桁は"0"で埋める
while (true)
{ //8バイトまで"0"文字で埋める
if (filesize_str.Length >= 8) { break; }
filesize_str = "0" + filesize_str; //空白桁に"0"を付ける。
}
v = "chk_sum File : " + filename + "chk_sum : " + filesize_str;
//チェックサムを、正しいものに書き換える。
tar_write_binary.AddRange(System.Text.Encoding.ASCII.GetBytes(filesize_str));
for (int k = 0; k < tar_write_binary.Count; k += 1)
{
int idx2 = k + chksum_pos;
ary_tar_file[idx2] = tar_write_binary[k];
}
tar_write_binary.Clear();
//await fs.WriteAsync((byte[])ary_tar_file.ToArray(), 0, ary_tar_file.Count);
//await fs.FlushAsync();
fs.Write((byte[])ary_tar_file.ToArray(), 0, ary_tar_file.Count);
//配列を、一旦、クリアする。
ary_tar_file.Clear();
tar_write_binary.Clear();
v = "write File : " + filename + "chk_sum : " + filesize_str;
//■本体ファイルを書き込む-------ここから--------------------------
//http://dobon.net/vb/dotnet/file/filestream.html
//☆☆☆512の倍数でなければならない。☆☆☆
//ファイルを開く
byte[] buffer = new byte[512];
int len = 0; //今回読み込んだバイト数の格納
//https://stackoverflow.com/questions/2436385/android-getting-from-a-uri-to-an-inputstream-to-a-byte-array/2436413
using (var inputStream = ac.ContentResolver.OpenInputStream(Archive_filepaths[i]))
{
using (Java.IO.BufferedInputStream stream = new Java.IO.BufferedInputStream(inputStream))
{
//ファイルを開く
//http://d.hatena.ne.jp/curest/20090829/1251532479
while ((len = stream.Read(buffer, 0, buffer.Length)) != -1)
{ //ファイルを読み込む
if (len == 0) { break; } //終端で出る
//部分的に読み込んだデータをそのまま受け渡す
//await fs.WriteAsync(buffer, 0, buffer.Length);
//await fs.FlushAsync();
fs.Write(buffer, 0, buffer.Length);
//配列初期化
Array.Fill<byte>(buffer, 0);
}
//閉じる
stream.Close();
}
inputStream.Close();
}
}
//■フッターの作成------------ここから-----------------
//1024バイトの空白。が、フッター
byte[] buffer2 = new byte[1024];
await fs.WriteAsync(buffer2, 0, buffer2.Length);
await fs.FlushAsync();
//■後処理
fs.Close(); //一旦ファイルを閉じる
}
//■■ここから■■■■tarをgzipで圧縮■■■■■
using (FileStream fs = File.Open(temp_name, FileMode.Open))
{ //出来たばかりのtarを再度開く
err_string = "9";
using (var compressedFileStream = ac.ContentResolver.OpenOutputStream(Output_Path))
{
err_string = "10";
using (var compressor = new GZipStream(compressedFileStream, CompressionMode.Compress))
{
await fs.CopyToAsync(compressor);
compressor.Close();
}
compressedFileStream.Close();
}
fs.Close();
}
//一時ファイルであるtarの削除
System.IO.File.Delete(temp_name);
Android.Widget.Toast.MakeText(Android.App.Application.Context, "tar.gz処理が終わりました", Android.Widget.ToastLength.Long).Show();
}
catch {
//メッセージ表示(保存先表示)
Android.Widget.Toast.MakeText(Android.App.Application.Context, "※else_Err!※" + err_string + "※", Android.Widget.ToastLength.Long).Show();
}
}
public async System.Threading.Tasks.Task<string> getRealPathFromURI(Activity ac, Android.Net.Uri contentUri)
{ //Uriからファイル名を取得する。
//https://codeday.me/jp/qa/20181204/12998.html
Android.Database.ICursor cursor = null;
if (contentUri.Scheme.Equals("content"))
{ //コンテントタイプのUriの場合
try
{ //https://stackoverflow.com/questions/5568874/how-to-extract-the-file-name-from-uri-returned-from-intent-action-get-content
cursor = ac.ContentResolver.Query(contentUri, null, null, null, null);
if (cursor != null && cursor.MoveToFirst())
{
//ファイル名とサイズを取得しておく
string fff = cursor.GetString(cursor.GetColumnIndex(Android.Provider.OpenableColumns.DisplayName));
filesize_str = cursor.GetString(cursor.GetColumnIndex(Android.Provider.OpenableColumns.Size));
await System.Threading.Tasks.Task.Delay(30);
cursor.Close();
return fff;
}
}
finally
{
if (cursor != null)
{
cursor.Close();
}
}
}
else
{ //ファイルタイプのUriの場合
string result = contentUri.Path;
filesize_str = System.IO.File.ReadAllBytes(result).ToString();
result = result.Substring(result.LastIndexOf('/') + 1);
return result;
}
return "___";
}
}
}
↓上記のコードの発動例
using System;
using Android.App;
using Android.OS;
using Android.Runtime;
using Android.Views;
using AndroidX.AppCompat.Widget;
using AndroidX.AppCompat.App;
using Google.Android.Material.FloatingActionButton;
using Google.Android.Material.Snackbar;
using Android.Widget;
using Android.Content;
using System.IO.Compression;
namespace App1
{
[Activity(Label = "@string/app_name", Theme = "@style/AppTheme.NoActionBar", MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
clsTarArchive clsTar = new clsTarArchive();
public System.Collections.Generic.List<Android.Net.Uri> file_selected_uris;
GZipStream bz2Stream;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
SetContentView(Resource.Layout.activity_main);
//Android11.0以上の場合のみ
string[] Manifest_Permissions = { Android.Manifest.Permission.WriteExternalStorage,
Android.Manifest.Permission.ReadExternalStorage,
Android.Manifest.Permission.AccessMediaLocation
};
//各権限をループ2
foreach (System.String Permission_str in Manifest_Permissions)
{
//https://docs.microsoft.com/ja-jp/xamarin/android/app-fundamentals/permissions?tabs=windows
//https://www.petitmonte.com/java/android_fileprovider.html
if (ApplicationContext.CheckCallingOrSelfPermission(Permission_str) !=
Android.Content.PM.Permission.Granted)
{ //許可されていない場合
// ストレージの権限の許可を求めるダイアログを表示する
//https://qiita.com/khara_nasuo486/items/f23c91ccd37db885aefe
if (AndroidX.Core.App.ActivityCompat.ShouldShowRequestPermissionRationale(this,
Permission_str))
{
AndroidX.Core.App.ActivityCompat.RequestPermissions(this,
Manifest_Permissions, (int)Android.Content.PM.RequestedPermission.Required);
}
else
{
Toast toast =
Toast.MakeText(ApplicationContext, "アプリ実行の権限が必要です", ToastLength.Long);
toast.Show();
AndroidX.Core.App.ActivityCompat.RequestPermissions(this,
Manifest_Permissions,
(int)Android.Content.PM.RequestedPermission.Required);
}
}
}
Button btn_main = FindViewById<Button>(Resource.Id.btn_main);
btn_main.Click += delegate
{
Intent imageIntent = new Intent(Intent.ActionGetContent);
imageIntent.SetType("*/*");
//複数画像選択かどうか?0なら複数選択
//https://stackoverflow.com/questions/19585815/select-multiple-images-from-android-gallery
//PDFとzipは複数。
imageIntent.PutExtra(Intent.ExtraAllowMultiple, true);
imageIntent.SetAction(Intent.ActionGetContent);
StartActivityForResult(
Intent.CreateChooser(imageIntent, "アーカイブするファイル選択してください"), 0);
};
}
protected override async void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
string err_string = "500";
try
{
//◆◆◆以下、画像選択時のイベント◆◆◆
if (requestCode == 0) {
if (resultCode == Result.Ok)
{
file_selected_uris = new System.Collections.Generic.List<Android.Net.Uri>();
file_selected_uris.Clear();
err_string = "501";
if (data.ClipData != null)
{ //複数選択された場合
int count = data.ClipData.ItemCount;
int currentItem = 0;
while (currentItem < count)
{ //各ファイルのURIを取得
currentItem = currentItem + 1;
file_selected_uris.Add(data.ClipData.GetItemAt(currentItem - 1).Uri);
}
}
else if (data.Data != null)
{ //1つだけの選択時
file_selected_uris.Add(data.Data);
}
err_string = "502";
//Windowsでいう所の「名前を付けて保存」ダイアログの作成
Intent intent = new Intent(Intent.ActionCreateDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("application/gzip");
//ファイル名は提案するだけで保存先などは最終的にこのダイアログでユーザーに決められる
intent.PutExtra(Intent.ExtraTitle, System.IO.Path.GetFileName("test.tar.gz"));
err_string = "503";
//今回はダイアログが終わると200を足した値で帰ってくる
StartActivityForResult(Intent.CreateChooser(intent, "保存先の指定"), 200);
return;
}
}
else if (requestCode == 200)
{
if (resultCode == Result.Ok)
{ //OK(ズドン)
err_string = "504" + (data.Data == null) ;
await clsTar.Convert_File_to_tar_gz(
data.Data, file_selected_uris.ToArray(), this);
}
}
}
catch {
//メッセージ表示(保存先表示)
Android.Widget.Toast.MakeText(Android.App.Application.Context, "※else_Err!※" + err_string + "※", Android.Widget.ToastLength.Long).Show();
}
}
public override bool OnCreateOptionsMenu(IMenu menu)
{
return true;
}
public override bool OnOptionsItemSelected(IMenuItem item)
{
int id = item.ItemId;
return base.OnOptionsItemSelected(item);
}
}
}
動作テスト
以上の方法で、作成したtar.gzは、
・TAR ファイルをオンラインで無料で開きます。迅速と安全! - ezyZip
以上のサービスでは、解凍できましたが。
ソフト・サービスによっては、
「破損ファイル」として、扱われる可能性もございます。
何卒、ご了承いただきますように、
お願い申し上げます。