Merge tag 'v23.04.01'

PiSCSI version 23.04.01
This commit is contained in:
Tony Kuker 2023-04-22 19:48:53 -05:00
commit 57fa874d53
38 changed files with 2200 additions and 2604 deletions

View File

@ -93,28 +93,16 @@ jobs:
working-directory: cpp working-directory: cpp
run: gcov --preserve-paths $(find -name '*.gcno') run: gcov --preserve-paths $(find -name '*.gcno')
- uses: actions/cache@v3
name: Cache SonarCloud scan cache
id: sonar-scan-cache
with:
path: ~/.sonar_cache/
key: sonar-scan-cache-${{ env.SONAR_SCANNER_VERSION }}-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
restore-keys: |
sonar-scan-cache-${{ env.SONAR_SCANNER_VERSION }}-${{ github.ref_name }}
- name: Run sonar-scanner - name: Run sonar-scanner
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: >- run: >-
(mkdir -p $HOME/.sonar_cache || true) &&
$HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin/sonar-scanner $HOME/.sonar/sonar-scanner-${{ env.SONAR_SCANNER_VERSION }}-linux/bin/sonar-scanner
--define sonar.host.url="${{ env.SONAR_SERVER_URL }}" --define sonar.host.url="${{ env.SONAR_SERVER_URL }}"
--define sonar.projectKey=${{ env.SONAR_PROJECT_KEY }} --define sonar.projectKey=${{ env.SONAR_PROJECT_KEY }}
--define sonar.organization=${{ env.SONAR_ORGANIZATION }} --define sonar.organization=${{ env.SONAR_ORGANIZATION }}
--define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}" --define sonar.cfamily.build-wrapper-output="${{ env.BUILD_WRAPPER_OUT_DIR }}"
--define sonar.cfamily.gcov.reportsPath=. --define sonar.cfamily.gcov.reportsPath=.
--define sonar.cfamily.cache.enabled=true
--define sonar.cfamily.cache.path="$HOME/.sonar_cache/"
--define sonar.coverage.exclusions="cpp/**/test/**" --define sonar.coverage.exclusions="cpp/**/test/**"
--define sonar.cpd.exclusions="cpp/**/test/**" --define sonar.cpd.exclusions="cpp/**/test/**"
--define sonar.inclusions="cpp/**,python/**" --define sonar.inclusions="cpp/**,python/**"

View File

@ -199,22 +199,22 @@ docs: $(DOC_DIR)/piscsi_man_page.txt $(DOC_DIR)/scsictl_man_page.txt $(DOC_DIR)/
$(SRC_PISCSI_CORE) $(SRC_SCSICTL_CORE) : $(OBJ_GENERATED) $(SRC_PISCSI_CORE) $(SRC_SCSICTL_CORE) : $(OBJ_GENERATED)
$(BINDIR)/$(PISCSI): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR) $(BINDIR)/$(PISCSI): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_PISCSI) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs
$(BINDIR)/$(SCSICTL): $(SRC_GENERATED) $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR) $(BINDIR)/$(SCSICTL): $(SRC_GENERATED) $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lprotobuf $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SCSICTL_CORE) $(OBJ_SCSICTL) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lprotobuf
$(BINDIR)/$(SCSIDUMP): $(OBJ_SCSIDUMP) $(OBJ_SHARED) | $(BINDIR) $(BINDIR)/$(SCSIDUMP): $(OBJ_SCSIDUMP) $(OBJ_SHARED) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSIDUMP) $(OBJ_SHARED) $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SCSIDUMP) $(OBJ_SHARED)
$(BINDIR)/$(SCSIMON): $(OBJ_SCSIMON) $(OBJ_SHARED) | $(BINDIR) $(BINDIR)/$(SCSIMON): $(OBJ_SCSIMON) $(OBJ_SHARED) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SCSIMON) $(OBJ_SHARED) $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SCSIMON) $(OBJ_SHARED)
$(BINDIR)/$(PISCSI_TEST): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SCSICTL_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
$(CXX) $(CXXFLAGS) $(TEST_WRAPS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs -lgmock -lgtest
$(BINDIR)/$(SCSILOOP): $(OBJ_SHARED) $(OBJ_SCSILOOP) | $(BINDIR) $(BINDIR)/$(SCSILOOP): $(OBJ_SHARED) $(OBJ_SCSILOOP) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_SHARED) $(OBJ_SCSILOOP) $(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $(OBJ_SHARED) $(OBJ_SCSILOOP)
$(BINDIR)/$(PISCSI_TEST): $(SRC_GENERATED) $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SCSICTL_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) | $(BINDIR)
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(TEST_WRAPS) -o $@ $(OBJ_PISCSI_CORE) $(OBJ_SCSICTL_CORE) $(OBJ_PISCSI_TEST) $(OBJ_SHARED) $(OBJ_PROTOBUF) $(OBJ_GENERATED) -lpthread -lpcap -lprotobuf -lstdc++fs -lgmock -lgtest
# Phony rules for building individual utilities # Phony rules for building individual utilities
.PHONY: $(PISCSI) $(SCSICTL) $(SCSIDUMP) $(SCSIMON) $(PISCSI_TEST) $(SCSILOOP) .PHONY: $(PISCSI) $(SCSICTL) $(SCSIDUMP) $(SCSIMON) $(PISCSI_TEST) $(SCSILOOP)

View File

@ -59,6 +59,7 @@ DeviceFactory::DeviceFactory()
extension_mapping["hdr"] = SCRM; extension_mapping["hdr"] = SCRM;
extension_mapping["mos"] = SCMO; extension_mapping["mos"] = SCMO;
extension_mapping["iso"] = SCCD; extension_mapping["iso"] = SCCD;
extension_mapping["is1"] = SCCD;
device_mapping["bridge"] = SCBR; device_mapping["bridge"] = SCBR;
device_mapping["daynaport"] = SCDP; device_mapping["daynaport"] = SCDP;
@ -118,7 +119,8 @@ shared_ptr<PrimaryDevice> DeviceFactory::CreateDevice(PbDeviceType type, int lun
break; break;
case SCCD: case SCCD:
device = make_shared<SCSICD>(lun, sector_sizes.find(SCCD)->second); device = make_shared<SCSICD>(lun, sector_sizes.find(SCCD)->second,
GetExtensionLowerCase(filename) == "is1" ? scsi_level::SCSI_1_CCS : scsi_level::SCSI_2);
device->SetProduct("SCSI CD-ROM"); device->SetProduct("SCSI CD-ROM");
break; break;

View File

@ -396,7 +396,12 @@ void Disk::SetUpModePages(map<int, vector<byte>>& pages, int page, bool changeab
void Disk::AddErrorPage(map<int, vector<byte>>& pages, bool) const void Disk::AddErrorPage(map<int, vector<byte>>& pages, bool) const
{ {
// Retry count is 0, limit time uses internal default value // Retry count is 0, limit time uses internal default value
pages[1] = vector<byte>(12); vector<byte> buf(12);
// TB, PER, DTE (required for OpenVMS/VAX compatibility, see issue #1117)
buf[2] = (byte)0x26;
pages[1] = buf;
} }
void Disk::AddFormatPage(map<int, vector<byte>>& pages, bool changeable) const void Disk::AddFormatPage(map<int, vector<byte>>& pages, bool changeable) const

View File

@ -21,7 +21,8 @@
using namespace scsi_defs; using namespace scsi_defs;
using namespace scsi_command_util; using namespace scsi_command_util;
SCSICD::SCSICD(int lun, const unordered_set<uint32_t>& sector_sizes) : Disk(SCCD, lun) SCSICD::SCSICD(int lun, const unordered_set<uint32_t>& sector_sizes, scsi_defs::scsi_level level)
: Disk(SCCD, lun), scsi_level(level)
{ {
SetSectorSizes(sector_sizes); SetSectorSizes(sector_sizes);
@ -164,7 +165,7 @@ void SCSICD::ReadToc()
vector<uint8_t> SCSICD::InquiryInternal() const vector<uint8_t> SCSICD::InquiryInternal() const
{ {
return HandleInquiry(device_type::CD_ROM, scsi_level::SCSI_2, true); return HandleInquiry(device_type::CD_ROM, scsi_level, true);
} }
void SCSICD::SetUpModePages(map<int, vector<byte>>& pages, int page, bool changeable) const void SCSICD::SetUpModePages(map<int, vector<byte>>& pages, int page, bool changeable) const

View File

@ -22,7 +22,7 @@ class SCSICD : public Disk, private ScsiMmcCommands
{ {
public: public:
SCSICD(int, const unordered_set<uint32_t>&); SCSICD(int, const unordered_set<uint32_t>&, scsi_defs::scsi_level = scsi_level::SCSI_2);
~SCSICD() override = default; ~SCSICD() override = default;
bool Init(const unordered_map<string, string>&) override; bool Init(const unordered_map<string, string>&) override;
@ -43,6 +43,7 @@ private:
void AddCDROMPage(map<int, vector<byte>>&, bool) const; void AddCDROMPage(map<int, vector<byte>>&, bool) const;
void AddCDDAPage(map<int, vector<byte>>&, bool) const; void AddCDDAPage(map<int, vector<byte>>&, bool) const;
scsi_defs::scsi_level scsi_level;
void OpenIso(); void OpenIso();
void OpenPhysical(); void OpenPhysical();

View File

@ -36,9 +36,9 @@ string SCSIHD::GetProductData() const
uint64_t capacity = GetBlockCount() * GetSectorSizeInBytes(); uint64_t capacity = GetBlockCount() * GetSectorSizeInBytes();
string unit; string unit;
// 10 GiB and more // 10,000 MiB and more
if (capacity >= 1'099'511'627'776) { if (capacity >= 10'485'760'000) {
capacity /= 1'099'511'627'776; capacity /= 1'073'741'824;
unit = "GiB"; unit = "GiB";
} }
// 1 MiB and more // 1 MiB and more

View File

@ -87,7 +87,7 @@ bool GPIOBUS_Raspberry::Init(mode_e mode)
// Map peripheral region memory // Map peripheral region memory
void *map = mmap(NULL, 0x1000100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, baseaddr); void *map = mmap(NULL, 0x1000100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, baseaddr);
if (map == MAP_FAILED) { if (map == MAP_FAILED) {
LOGERROR("Error: Unable to map memory") LOGERROR("Error: Unable to map memory: %s", strerror(errno))
close(fd); close(fd);
return false; return false;
} }
@ -985,4 +985,4 @@ uint32_t GPIOBUS_Raspberry::Acquire()
#endif // SIGNAL_CONTROL_MODE #endif // SIGNAL_CONTROL_MODE
return signals; return signals;
} }

View File

@ -62,7 +62,8 @@ void Piscsi::Banner(const vector<char *>& args) const
cout << " hdi : SCSI HD image (Anex86 HD image)\n"; cout << " hdi : SCSI HD image (Anex86 HD image)\n";
cout << " nhd : SCSI HD image (T98Next HD image)\n"; cout << " nhd : SCSI HD image (T98Next HD image)\n";
cout << " mos : SCSI MO image (MO image)\n"; cout << " mos : SCSI MO image (MO image)\n";
cout << " iso : SCSI CD image (ISO 9660 image)\n" << flush; cout << " iso : SCSI CD image (ISO 9660 image)\n";
cout << " is1 : SCSI CD image (ISO 9660 image, SCSI-1)\n" << flush;
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
} }

View File

@ -14,7 +14,7 @@
// The following should be updated for each release // The following should be updated for each release
const int piscsi_major_version = 23; // Last two digits of year const int piscsi_major_version = 23; // Last two digits of year
const int piscsi_minor_version = 2; // Month const int piscsi_minor_version = 4; // Month
const int piscsi_patch_version = 1; // Patch number - increment for each update const int piscsi_patch_version = 1; // Patch number - increment for each update
using namespace std; using namespace std;

View File

@ -29,6 +29,7 @@ TEST(DeviceFactoryTest, GetTypeForFile)
EXPECT_EQ(device_factory.GetTypeForFile("test.hdr"), SCRM); EXPECT_EQ(device_factory.GetTypeForFile("test.hdr"), SCRM);
EXPECT_EQ(device_factory.GetTypeForFile("test.mos"), SCMO); EXPECT_EQ(device_factory.GetTypeForFile("test.mos"), SCMO);
EXPECT_EQ(device_factory.GetTypeForFile("test.iso"), SCCD); EXPECT_EQ(device_factory.GetTypeForFile("test.iso"), SCCD);
EXPECT_EQ(device_factory.GetTypeForFile("test.is1"), SCCD);
EXPECT_EQ(device_factory.GetTypeForFile("test.suffix.iso"), SCCD); EXPECT_EQ(device_factory.GetTypeForFile("test.suffix.iso"), SCCD);
EXPECT_EQ(device_factory.GetTypeForFile("bridge"), SCBR); EXPECT_EQ(device_factory.GetTypeForFile("bridge"), SCBR);
EXPECT_EQ(device_factory.GetTypeForFile("daynaport"), SCDP); EXPECT_EQ(device_factory.GetTypeForFile("daynaport"), SCDP);
@ -79,7 +80,7 @@ TEST(DeviceFactoryTest, GetExtensionMapping)
DeviceFactory device_factory; DeviceFactory device_factory;
unordered_map<string, PbDeviceType> mapping = device_factory.GetExtensionMapping(); unordered_map<string, PbDeviceType> mapping = device_factory.GetExtensionMapping();
EXPECT_EQ(9, mapping.size()); EXPECT_EQ(10, mapping.size());
EXPECT_EQ(SCHD, mapping["hd1"]); EXPECT_EQ(SCHD, mapping["hd1"]);
EXPECT_EQ(SCHD, mapping["hds"]); EXPECT_EQ(SCHD, mapping["hds"]);
EXPECT_EQ(SCHD, mapping["hda"]); EXPECT_EQ(SCHD, mapping["hda"]);
@ -89,6 +90,7 @@ TEST(DeviceFactoryTest, GetExtensionMapping)
EXPECT_EQ(SCRM, mapping["hdr"]); EXPECT_EQ(SCRM, mapping["hdr"]);
EXPECT_EQ(SCMO, mapping["mos"]); EXPECT_EQ(SCMO, mapping["mos"]);
EXPECT_EQ(SCCD, mapping["iso"]); EXPECT_EQ(SCCD, mapping["iso"]);
EXPECT_EQ(SCCD, mapping["is1"]);
} }
TEST(DeviceFactoryTest, GetDefaultParams) TEST(DeviceFactoryTest, GetDefaultParams)

View File

@ -3,7 +3,7 @@
// SCSI Target Emulator PiSCSI // SCSI Target Emulator PiSCSI
// for Raspberry Pi // for Raspberry Pi
// //
// Copyright (C) 2022 Uwe Seimet // Copyright (C) 2022-2023 Uwe Seimet
// //
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
@ -358,6 +358,7 @@ class MockSCSIHD : public SCSIHD //NOSONAR Ignore inheritance hierarchy depth in
class MockSCSIHD_NEC : public SCSIHD_NEC //NOSONAR Ignore inheritance hierarchy depth in unit tests class MockSCSIHD_NEC : public SCSIHD_NEC //NOSONAR Ignore inheritance hierarchy depth in unit tests
{ {
FRIEND_TEST(ScsiHdNecTest, SetUpModePages); FRIEND_TEST(ScsiHdNecTest, SetUpModePages);
FRIEND_TEST(ScsiHdNecTest, TestAddErrorPage);
FRIEND_TEST(ScsiHdNecTest, TestAddFormatPage); FRIEND_TEST(ScsiHdNecTest, TestAddFormatPage);
FRIEND_TEST(ScsiHdNecTest, TestAddDrivePage); FRIEND_TEST(ScsiHdNecTest, TestAddDrivePage);
FRIEND_TEST(PiscsiExecutorTest, ProcessDeviceCmd); FRIEND_TEST(PiscsiExecutorTest, ProcessDeviceCmd);

View File

@ -39,6 +39,8 @@ TEST(PiscsiImageTest, CreateImage)
PbCommand command; PbCommand command;
PiscsiImage image; PiscsiImage image;
StorageDevice::UnreserveAll();
EXPECT_FALSE(image.CreateImage(context, command)) << "Filename must be reported as missing"; EXPECT_FALSE(image.CreateImage(context, command)) << "Filename must be reported as missing";
SetParam(command, "file", "/a/b/c/filename"); SetParam(command, "file", "/a/b/c/filename");
@ -63,6 +65,8 @@ TEST(PiscsiImageTest, DeleteImage)
PbCommand command; PbCommand command;
PiscsiImage image; PiscsiImage image;
StorageDevice::UnreserveAll();
EXPECT_FALSE(image.DeleteImage(context, command)) << "Filename must be reported as missing"; EXPECT_FALSE(image.DeleteImage(context, command)) << "Filename must be reported as missing";
SetParam(command, "file", "/a/b/c/filename"); SetParam(command, "file", "/a/b/c/filename");
@ -82,6 +86,8 @@ TEST(PiscsiImageTest, RenameImage)
PbCommand command; PbCommand command;
PiscsiImage image; PiscsiImage image;
StorageDevice::UnreserveAll();
EXPECT_FALSE(image.RenameImage(context, command)) << "Source filename must be reported as missing"; EXPECT_FALSE(image.RenameImage(context, command)) << "Source filename must be reported as missing";
SetParam(command, "from", "/a/b/c/filename_from"); SetParam(command, "from", "/a/b/c/filename_from");
@ -99,6 +105,8 @@ TEST(PiscsiImageTest, CopyImage)
PbCommand command; PbCommand command;
PiscsiImage image; PiscsiImage image;
StorageDevice::UnreserveAll();
EXPECT_FALSE(image.CopyImage(context, command)) << "Source filename must be reported as missing"; EXPECT_FALSE(image.CopyImage(context, command)) << "Source filename must be reported as missing";
SetParam(command, "from", "/a/b/c/filename_from"); SetParam(command, "from", "/a/b/c/filename_from");
@ -116,6 +124,8 @@ TEST(PiscsiImageTest, SetImagePermissions)
PbCommand command; PbCommand command;
PiscsiImage image; PiscsiImage image;
StorageDevice::UnreserveAll();
EXPECT_FALSE(image.SetImagePermissions(context, command)) << "Filename must be reported as missing"; EXPECT_FALSE(image.SetImagePermissions(context, command)) << "Filename must be reported as missing";
SetParam(command, "file", "/a/b/c/filename"); SetParam(command, "file", "/a/b/c/filename");

View File

@ -221,5 +221,5 @@ TEST(PiscsiResponseTest, GetMappingInfo)
const auto& info = response.GetMappingInfo(result); const auto& info = response.GetMappingInfo(result);
EXPECT_TRUE(result.status()); EXPECT_TRUE(result.status());
EXPECT_EQ(9, info->mapping().size()); EXPECT_EQ(10, info->mapping().size());
} }

View File

@ -30,6 +30,8 @@ void ScsiCdTest_SetUpModePages(map<int, vector<byte>>& pages)
TEST(ScsiCdTest, Inquiry) TEST(ScsiCdTest, Inquiry)
{ {
TestInquiry(SCCD, device_type::CD_ROM, scsi_level::SCSI_2, "PiSCSI SCSI CD-ROM ", 0x1f, true); TestInquiry(SCCD, device_type::CD_ROM, scsi_level::SCSI_2, "PiSCSI SCSI CD-ROM ", 0x1f, true);
TestInquiry(SCCD, device_type::CD_ROM, scsi_level::SCSI_1_CCS, "PiSCSI SCSI CD-ROM ", 0x1f, true, ".is1");
} }
TEST(ScsiCdTest, SetUpModePages) TEST(ScsiCdTest, SetUpModePages)

View File

@ -3,7 +3,7 @@
// SCSI Target Emulator PiSCSI // SCSI Target Emulator PiSCSI
// for Raspberry Pi // for Raspberry Pi
// //
// Copyright (C) 2022 Uwe Seimet // Copyright (C) 2022-2023 Uwe Seimet
// //
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
@ -47,6 +47,20 @@ TEST(ScsiHdNecTest, SetUpModePages)
ScsiHdNecTest_SetUpModePages(pages); ScsiHdNecTest_SetUpModePages(pages);
} }
TEST(ScsiHdNecTest, TestAddErrorPage)
{
map<int, vector<byte>> pages;
MockSCSIHD_NEC hd(0);
hd.SetBlockCount(0x1234);
hd.SetReady(true);
// Non changeable
hd.SetUpModePages(pages, 0x01, false);
EXPECT_EQ(1, pages.size()) << "Unexpected number of mode pages";
const vector<byte>& page_1 = pages[1];
EXPECT_EQ(0x26, to_integer<int>(page_1[2]));
}
TEST(ScsiHdNecTest, TestAddFormatPage) TEST(ScsiHdNecTest, TestAddFormatPage)
{ {
map<int, vector<byte>> pages; map<int, vector<byte>> pages;

View File

@ -69,10 +69,10 @@ TEST(ScsiHdTest, GetProductData)
hd_gb.SetFilename(string(filename)); hd_gb.SetFilename(string(filename));
hd_gb.SetSectorSizeInBytes(1024); hd_gb.SetSectorSizeInBytes(1024);
hd_gb.SetBlockCount(1'099'511'627'776 / 1024); hd_gb.SetBlockCount(10'737'418'240 / 1024);
hd_gb.FinalizeSetup(0); hd_gb.FinalizeSetup(0);
s = hd_gb.GetProduct(); s = hd_gb.GetProduct();
EXPECT_NE(string::npos, s.find("1 GiB")); EXPECT_NE(string::npos, s.find("10 GiB"));
remove(filename); remove(filename);
} }

View File

@ -34,6 +34,7 @@ PiSCSI will determine the type of device based upon the file extension of the FI
hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers) hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers)
mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.) mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.)
iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image) iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image)
is1: SCSI CD-ROM or DVD-ROM image (ISO 9660 image, SCSI-1)
For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command: For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command:
sudo piscsi -ID0 /path/to/drive/hdimage.hda sudo piscsi -ID0 /path/to/drive/hdimage.hda

View File

@ -29,6 +29,7 @@ DESCRIPTION
hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers) hda: SCSI Hard Disk image (Apple compatible - typically used with Macintosh computers)
mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.) mos: SCSI Magneto-Optical image (generic - typically used with NeXT, X68000, etc.)
iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image) iso: SCSI CD-ROM or DVD-ROM image (ISO 9660 image)
is1: SCSI CD-ROM or DVD-ROM image (ISO 9660 image, SCSI-1)
For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command: For example, if you want to specify an Apple-compatible HD image on ID 0, you can use the following command:
sudo piscsi -ID0 /path/to/drive/hdimage.hda sudo piscsi -ID0 /path/to/drive/hdimage.hda

View File

@ -49,8 +49,7 @@ echo -e $logo
CONNECT_TYPE="FULLSPEC" CONNECT_TYPE="FULLSPEC"
# clang v11 is the latest distributed by Buster # clang v11 is the latest distributed by Buster
COMPILER="clang++-11" COMPILER="clang++-11"
# Takes half of the CPU cores available, to avoid running out of memory on low spec devices CORES=1
CORES=$(awk 'BEGIN { x = '$(nproc)'; y = 2; print (x / y) }' | numfmt --round=up --format=%.0f)
USER=$(whoami) USER=$(whoami)
BASE=$(dirname "$(readlink -f "${0}")") BASE=$(dirname "$(readlink -f "${0}")")
CPP_PATH="$BASE/cpp" CPP_PATH="$BASE/cpp"
@ -87,6 +86,12 @@ function initialChecks() {
fi fi
} }
# Only to be used for pi-gen automated install
function cacheSudo() {
echo "Caching sudo password"
echo raspberry | sudo -v -S
}
# checks that the current user has sudoers privileges # checks that the current user has sudoers privileges
function sudoCheck() { function sudoCheck() {
if [[ $HEADLESS ]]; then if [[ $HEADLESS ]]; then
@ -705,10 +710,34 @@ function setupWirelessNetworking() {
sudo reboot sudo reboot
} }
# Detects or creates the file sharing directory
function createFileSharingDir() {
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
echo
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
echo
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
else
exit 0
fi
elif [ -d "$FILE_SHARE_PATH" ]; then
echo "Found a $FILE_SHARE_PATH directory; will use it for file sharing."
else
echo "Creating the $FILE_SHARE_PATH directory and granting read/write permissions to all users..."
sudo mkdir -p "$FILE_SHARE_PATH"
sudo chown -R "$USER:$USER" "$FILE_SHARE_PATH"
chmod -Rv 775 "$FILE_SHARE_PATH"
fi
}
# Downloads, compiles, and installs Netatalk (AppleShare server) # Downloads, compiles, and installs Netatalk (AppleShare server)
function installNetatalk() { function installNetatalk() {
NETATALK_VERSION="2-230201" NETATALK_VERSION="230302"
NETATALK_CONFIG_PATH="/etc/netatalk" NETATALK_CONFIG_PATH="/etc/netatalk"
NETATALK_OPTIONS="--cores=$CORES --share-name='$FILE_SHARE_NAME' --share-path='$FILE_SHARE_PATH'"
if [ -d "$NETATALK_CONFIG_PATH" ]; then if [ -d "$NETATALK_CONFIG_PATH" ]; then
echo echo
@ -723,27 +752,26 @@ function installNetatalk() {
fi fi
fi fi
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then echo
echo echo "Downloading tarball to $HOME..."
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH." cd $HOME || exit 1
echo wget -O "netatalk-2.$NETATALK_VERSION.tar.gz" "https://github.com/rdmark/netatalk-2.x/releases/download/netatalk-2-$NETATALK_VERSION/netatalk-2.$NETATALK_VERSION.tar.gz" </dev/null
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY echo "Unpacking tarball..."
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then tar -xzf "netatalk-2.$NETATALK_VERSION.tar.gz"
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1 rm "netatalk-2.$NETATALK_VERSION.tar.gz"
else
exit 0 if [ -f "/etc/network/interfaces.d/piscsi_bridge" ]; then
fi echo "PiSCSI network bridge detected. Using 'piscsi_bridge' interface for AppleTalk."
NETATALK_OPTIONS="$NETATALK_OPTIONS --appletalk-interface=piscsi_bridge"
fi fi
echo "Downloading netatalk-$NETATALK_VERSION to $HOME" [[ $HEADLESS ]] && NETATALK_OPTIONS="$NETATALK_OPTIONS --headless"
cd $HOME || exit 1 [[ $SKIP_PACKAGES ]] && NETATALK_OPTIONS="$NETATALK_OPTIONS --no-packages"
wget -O "netatalk-$NETATALK_VERSION.tar.gz" "https://github.com/rdmark/Netatalk-2.x/archive/refs/tags/netatalk-$NETATALK_VERSION.tar.gz" </dev/null [[ $SKIP_MAKE_CLEAN ]] && NETATALK_OPTIONS="$NETATALK_OPTIONS --no-make-clean"
tar -xzvf "netatalk-$NETATALK_VERSION.tar.gz"
rm "netatalk-$NETATALK_VERSION.tar.gz"
cd "$HOME/Netatalk-2.x-netatalk-$NETATALK_VERSION/contrib/shell_utils" || exit 1 cd "$HOME/netatalk-2.$NETATALK_VERSION/contrib/shell_utils" || exit 1
./debian_install.sh -j="$CORES" -n="$FILE_SHARE_NAME" -p="$FILE_SHARE_PATH" || exit 1 bash -c "./debian_install.sh $NETATALK_OPTIONS" || exit 1
} }
# Appends the images dir as a shared Netatalk volume # Appends the images dir as a shared Netatalk volume
@ -856,26 +884,6 @@ function installSamba() {
fi fi
fi fi
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
echo
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
echo
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
else
exit 0
fi
elif [ -d "$FILE_SHARE_PATH" ]; then
echo "Found a $FILE_SHARE_PATH directory; will use it for file sharing."
else
echo "Creating the $FILE_SHARE_PATH directory and granting read/write permissions to all users..."
sudo mkdir -p "$FILE_SHARE_PATH"
sudo chown -R "$USER:$USER" "$FILE_SHARE_PATH"
chmod -Rv 775 "$FILE_SHARE_PATH"
fi
echo "" echo ""
echo "Installing dependencies..." echo "Installing dependencies..."
sudo apt-get update || true sudo apt-get update || true
@ -1261,6 +1269,7 @@ function runChoice() {
;; ;;
7) 7)
echo "Installing AppleShare File Server" echo "Installing AppleShare File Server"
createFileSharingDir
installNetatalk installNetatalk
echo "Installing AppleShare File Server - Complete!" echo "Installing AppleShare File Server - Complete!"
;; ;;
@ -1272,6 +1281,7 @@ function runChoice() {
echo "WARNING: The FTP server may transfer unencrypted data over the network." echo "WARNING: The FTP server may transfer unencrypted data over the network."
echo "Proceed with this installation only if you are on a private, secure network." echo "Proceed with this installation only if you are on a private, secure network."
sudoCheck sudoCheck
createFileSharingDir
installFtp installFtp
echo "Installing FTP File Server - Complete!" echo "Installing FTP File Server - Complete!"
;; ;;
@ -1283,6 +1293,7 @@ function runChoice() {
echo " - Create a directory in the current user's home directory where shared files will be stored" echo " - Create a directory in the current user's home directory where shared files will be stored"
echo " - Create a Samba user for the current user" echo " - Create a Samba user for the current user"
sudoCheck sudoCheck
createFileSharingDir
installSamba installSamba
echo "Installing SMB File Server - Complete!" echo "Installing SMB File Server - Complete!"
;; ;;
@ -1359,6 +1370,25 @@ function runChoice() {
installPackagesStandalone installPackagesStandalone
compilePiscsi compilePiscsi
;; ;;
99)
echo "Hidden setup mode for running the pi-gen utility"
echo "This shouldn't be used by normal users"
sudoCache
createImagesDir
createCfgDir
updatePiscsiGit
installPackages
installHfdisk
fetchHardDiskDrivers
compilePiscsi
installPiscsi
enablePiscsiService
preparePythonCommon
cachePipPackages
installPiscsiWebInterface
installWebInterfaceService
echo "Automated install of the PiSCSI Service $(CONNECT_TYPE) complete!"
;;
-h|--help|h|help) -h|--help|h|help)
showMenu showMenu
;; ;;
@ -1372,7 +1402,7 @@ function runChoice() {
function readChoice() { function readChoice() {
choice=-1 choice=-1
until [ $choice -ge "0" ] && [ $choice -le "16" ]; do until [ $choice -ge "0" ] && ([ $choice -eq "99" ] || [ $choice -le "16" ]) ; do
echo -n "Enter your choice (0-16) or CTRL-C to exit: " echo -n "Enter your choice (0-16) or CTRL-C to exit: "
read -r choice read -r choice
done done

View File

@ -4,7 +4,7 @@ Module for methods reading from and writing to the file system
import logging import logging
import asyncio import asyncio
from os import walk from os import walk, path
from functools import lru_cache from functools import lru_cache
from pathlib import PurePath, Path from pathlib import PurePath, Path
from zipfile import ZipFile, is_zipfile from zipfile import ZipFile, is_zipfile
@ -57,7 +57,7 @@ class FileCmds:
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def list_config_files(self): def list_config_files(self):
""" """
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR. Finds files with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
Returns a (list) of (str) files_list Returns a (list) of (str) files_list
""" """
files_list = [] files_list = []
@ -67,6 +67,26 @@ class FileCmds:
files_list.append(file) files_list.append(file)
return files_list return files_list
# noinspection PyMethodMayBeStatic
def list_subdirs(self, directory):
"""
Finds subdirs within the (str) directory dir.
Returns a (list) of (str) subdir_list.
"""
subdir_list = []
# Filter out file sharing meta data dirs
excluded_dirs = ("Network Trash Folder", "Temporary Items", "TheVolumeSettingsFolder")
for root, dirs, _files in walk(directory, topdown=True):
# Strip out dirs that begin with .
dirs[:] = [d for d in dirs if not d[0] == "."]
for dir in dirs:
if dir not in excluded_dirs:
dirpath = path.join(root, dir)
subdir_list.append(dirpath.replace(directory, "", 1))
subdir_list.sort()
return subdir_list
def list_images(self): def list_images(self):
""" """
Sends a IMAGE_FILES_INFO command to the server Sends a IMAGE_FILES_INFO command to the server
@ -146,7 +166,15 @@ class FileCmds:
parameters = {"file_path": file_path} parameters = {"file_path": file_path}
if file_path.exists(): if file_path.exists():
file_path.unlink() try:
file_path.unlink()
except OSError as error:
logging.error(error)
return {
"status": False,
"return_code": ReturnCodes.DELETEFILE_UNABLE_TO_DELETE,
"parameters": parameters,
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.DELETEFILE_SUCCESS, "return_code": ReturnCodes.DELETEFILE_SUCCESS,
@ -159,18 +187,28 @@ class FileCmds:
} }
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def rename_file(self, file_path, target_path): def rename_file(self, file_path, target_path, overwrite_target=False):
""" """
Takes: Takes:
- (Path) file_path for the file to rename - (Path) file_path for the file to rename
- (Path) target_path for the name to rename - (Path) target_path for the name to rename
- optional (bool) overwrite_target
Returns (dict) with (bool) status, (str) msg, (dict) parameters Returns (dict) with (bool) status, (str) msg, (dict) parameters
""" """
parameters = {"target_path": target_path} parameters = {"target_path": target_path}
if not target_path.parent.exists(): if not target_path.parent.exists():
target_path.parent.mkdir(parents=True) target_path.parent.mkdir(parents=True)
if target_path.parent.exists() and not target_path.exists():
file_path.rename(target_path) if overwrite_target or not target_path.exists():
try:
file_path.rename(target_path)
except OSError as error:
logging.error(error)
return {
"status": False,
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE,
"parameters": parameters,
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.RENAMEFILE_SUCCESS, "return_code": ReturnCodes.RENAMEFILE_SUCCESS,
@ -178,23 +216,33 @@ class FileCmds:
} }
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE, "return_code": ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE,
"parameters": parameters, "parameters": parameters,
} }
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def copy_file(self, file_path, target_path): def copy_file(self, file_path, target_path, overwrite_target=False):
""" """
Takes: Takes:
- (Path) file_path for the file to copy from - (Path) file_path for the file to copy from
- (Path) target_path for the name to copy to - (Path) target_path for the name to copy to
- optional (bool) overwrite_target
Returns (dict) with (bool) status, (str) msg, (dict) parameters Returns (dict) with (bool) status, (str) msg, (dict) parameters
""" """
parameters = {"target_path": target_path} parameters = {"target_path": target_path}
if not target_path.parent.exists(): if not target_path.parent.exists():
target_path.parent.mkdir(parents=True) target_path.parent.mkdir(parents=True)
if target_path.parent.exists() and not target_path.exists():
copyfile(str(file_path), str(target_path)) if overwrite_target or not target_path.exists():
try:
copyfile(str(file_path), str(target_path))
except OSError as error:
logging.error(error)
return {
"status": False,
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
"parameters": parameters,
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.WRITEFILE_SUCCESS, "return_code": ReturnCodes.WRITEFILE_SUCCESS,
@ -202,32 +250,41 @@ class FileCmds:
} }
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE, "return_code": ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE,
"parameters": parameters, "parameters": parameters,
} }
def create_empty_image(self, target_path, size): def create_empty_image(self, target_path, size, overwrite_target=False):
""" """
Takes (Path) target_path and (int) size in bytes Creates a new empty binary file to use as image.
Creates a new empty binary file to use as image Takes:
- (Path) target_path
- (int) size in bytes
- optional (bool) overwrite_target
Returns (dict) with (bool) status, (str) msg, (dict) parameters Returns (dict) with (bool) status, (str) msg, (dict) parameters
""" """
parameters = {"target_path": target_path} parameters = {"target_path": target_path}
if not target_path.parent.exists(): if not target_path.parent.exists():
target_path.parent.mkdir(parents=True) target_path.parent.mkdir(parents=True)
if target_path.parent.exists() and not target_path.exists():
if overwrite_target or not target_path.exists():
try: try:
with open(f"{target_path}", "wb") as out: with open(f"{target_path}", "wb") as out:
out.seek(size - 1) out.seek(size - 1)
out.write(b"\0") out.write(b"\0")
except OSError as error: except OSError as error:
return {"status": False, "msg": str(error)} logging.error(error)
return {
"status": False,
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE,
"parameters": parameters,
}
return {"status": True, "msg": ""} return {"status": True, "msg": ""}
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.WRITEFILE_COULD_NOT_WRITE, "return_code": ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE,
"parameters": parameters, "parameters": parameters,
} }
@ -262,6 +319,7 @@ class FileCmds:
if self.rename_file( if self.rename_file(
Path(file["absolute_path"]), Path(file["absolute_path"]),
prop_path, prop_path,
overwrite_target=True,
): ):
properties_files_moved.append( properties_files_moved.append(
{ {
@ -317,56 +375,39 @@ class FileCmds:
server_info = self.piscsi.get_server_info() server_info = self.piscsi.get_server_info()
full_file_path = Path(server_info["image_dir"]) / file_name full_file_path = Path(server_info["image_dir"]) / file_name
# Inject hfdisk commands to create Drive with correct partitions # Inject hfdisk commands to create Mac partition table with HFS partitions
# https://www.codesrc.com/mediawiki/index.php/HFSFromScratch
# i initialize partition map
# continue with default first block
# C Create 1st partition with type specified next)
# continue with default
# 32 32 blocks (required for HFS+)
# Driver_Partition Partition Name
# Apple_Driver Partition Type (available types: Apple_Driver,
# Apple_Driver43, Apple_Free, Apple_HFS...)
# C Create 2nd partition with type specified next
# continue with default first block
# continue with default block size (rest of the disk)
# ${volumeName} Partition name provided by user
# Apple_HFS Partition Type
# w Write partition map to disk
# y Confirm partition table
# p Print partition map
if disk_format == "HFS": if disk_format == "HFS":
partitioning_tool = "hfdisk" partitioning_tool = "hfdisk"
commands = [ commands = [
"i", "i", # Initialize partition map
"", "", # Continue with default first block
"C", "C", # Create 1st partition with type specified next)
"", "", # Continue with default
"32", "32", # 32 block (required for HFS+)
"Driver_Partition", "Driver_Partition", # Partition Name
"Apple_Driver", "Apple_Driver", # Partition Type
"C", "C", # Create 2nd partition with type specified next
"", "", # Continue with default first block
"", "", # Continue with default block size (rest of the disk)
volume_name, volume_name, # Partition name
"Apple_HFS", "Apple_HFS", # Partition Type
"w", "w", # Write partition map to disk
"y", "y", # Confirm partition table
"p", "p", # Print partition map (for the log)
] ]
# Create a DOS label, primary partition, W95 FAT type # Inject fdisk commands to create primary FAT partition with MS-DOS label
elif disk_format == "FAT": elif disk_format == "FAT":
partitioning_tool = "fdisk" partitioning_tool = "fdisk"
commands = [ commands = [
"o", "o", # create a new empty DOS partition table
"n", "n", # add a new partition
"p", "p", # primary partition
"", "", # default partition number
"", "", # default first sector
"", "", # default last sector
"t", "t", # change partition type
"b", "b", # choose W95 FAT32 type
"w", "w", # write table to disk and exit
] ]
try: try:
process = Popen( process = Popen(

View File

@ -9,12 +9,14 @@ class ReturnCodes:
DELETEFILE_SUCCESS = 0 DELETEFILE_SUCCESS = 0
DELETEFILE_FILE_NOT_FOUND = 1 DELETEFILE_FILE_NOT_FOUND = 1
DELETEFILE_UNABLE_TO_DELETE = 2
RENAMEFILE_SUCCESS = 10 RENAMEFILE_SUCCESS = 10
RENAMEFILE_UNABLE_TO_MOVE = 11 RENAMEFILE_UNABLE_TO_MOVE = 11
DOWNLOADFILETOISO_SUCCESS = 20 DOWNLOADFILETOISO_SUCCESS = 20
DOWNLOADTODIR_SUCCESS = 30 DOWNLOADTODIR_SUCCESS = 30
WRITEFILE_SUCCESS = 40 WRITEFILE_SUCCESS = 40
WRITEFILE_COULD_NOT_WRITE = 41 WRITEFILE_COULD_NOT_WRITE = 41
WRITEFILE_COULD_NOT_OVERWRITE = 42
READCONFIG_SUCCESS = 50 READCONFIG_SUCCESS = 50
READCONFIG_COULD_NOT_READ = 51 READCONFIG_COULD_NOT_READ = 51
READCONFIG_INVALID_CONFIG_FILE_FORMAT = 52 READCONFIG_INVALID_CONFIG_FILE_FORMAT = 52

View File

@ -21,23 +21,8 @@ class SysCmds:
@staticmethod @staticmethod
def running_env(): def running_env():
""" """
Returns (str) git and (str) env Returns (str) env, with details on the system hardware and software
git contains the git hash of the checked out code
env is the various system information where this app is running
""" """
try:
ra_git_version = (
subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning(SHELL_ERROR, error.cmd, error.stderr.decode("utf-8"))
ra_git_version = ""
PROC_MODEL_PATH = "/proc/device-tree/model" PROC_MODEL_PATH = "/proc/device-tree/model"
SYS_VENDOR_PATH = "/sys/devices/virtual/dmi/id/sys_vendor" SYS_VENDOR_PATH = "/sys/devices/virtual/dmi/id/sys_vendor"
@ -67,7 +52,6 @@ class SysCmds:
env = uname() env = uname()
return { return {
"git": ra_git_version,
"env": f"{hardware}, {env.system} {env.release} {env.machine}", "env": f"{hardware}, {env.system} {env.release} {env.machine}",
} }

View File

@ -1,380 +0,0 @@
#!/usr/bin/env python3
# BSD 3-Clause License
#
# Copyright (c) 2021, akuker
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import RPi.GPIO as gpio
import time
pin_settle_delay = 0.01
err_count = 0
# Define constants for each of the SCSI signals, based upon their
# raspberry pi pin number (since we're using BOARD mode of RPi.GPIO)
scsi_d0_gpio = 19
scsi_d1_gpio = 23
scsi_d2_gpio = 32
scsi_d3_gpio = 33
scsi_d4_gpio = 8
scsi_d5_gpio = 10
scsi_d6_gpio = 36
scsi_d7_gpio = 11
scsi_dp_gpio = 12
scsi_atn_gpio = 35
scsi_rst_gpio = 38
scsi_ack_gpio = 40
scsi_req_gpio = 15
scsi_msg_gpio = 16
scsi_cd_gpio = 18
scsi_io_gpio = 22
scsi_bsy_gpio = 37
scsi_sel_gpio = 13
# Pin numbers of the direction controllers of the PiSCSI board
piscsi_ind_gpio = 31
piscsi_tad_gpio = 26
piscsi_dtd_gpio = 24
piscsi_none = -1
# Matrix showing all of the SCSI signals, along what signal they're looped back to.
# dir_ctrl indicates which direction control pin is associated with that output
gpio_map = [
{
"gpio_num": scsi_d0_gpio,
"attached_to": scsi_ack_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d1_gpio,
"attached_to": scsi_sel_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d2_gpio,
"attached_to": scsi_atn_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d3_gpio,
"attached_to": scsi_rst_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d4_gpio,
"attached_to": scsi_cd_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d5_gpio,
"attached_to": scsi_io_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d6_gpio,
"attached_to": scsi_msg_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_d7_gpio,
"attached_to": scsi_req_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_dp_gpio,
"attached_to": scsi_bsy_gpio,
"dir_ctrl": piscsi_dtd_gpio,
},
{
"gpio_num": scsi_atn_gpio,
"attached_to": scsi_d2_gpio,
"dir_ctrl": piscsi_ind_gpio,
},
{
"gpio_num": scsi_rst_gpio,
"attached_to": scsi_d3_gpio,
"dir_ctrl": piscsi_ind_gpio,
},
{
"gpio_num": scsi_ack_gpio,
"attached_to": scsi_d0_gpio,
"dir_ctrl": piscsi_ind_gpio,
},
{
"gpio_num": scsi_req_gpio,
"attached_to": scsi_d7_gpio,
"dir_ctrl": piscsi_tad_gpio,
},
{
"gpio_num": scsi_msg_gpio,
"attached_to": scsi_d6_gpio,
"dir_ctrl": piscsi_tad_gpio,
},
{
"gpio_num": scsi_cd_gpio,
"attached_to": scsi_d4_gpio,
"dir_ctrl": piscsi_tad_gpio,
},
{
"gpio_num": scsi_io_gpio,
"attached_to": scsi_d5_gpio,
"dir_ctrl": piscsi_tad_gpio,
},
{
"gpio_num": scsi_bsy_gpio,
"attached_to": scsi_dp_gpio,
"dir_ctrl": piscsi_tad_gpio,
},
{
"gpio_num": scsi_sel_gpio,
"attached_to": scsi_d1_gpio,
"dir_ctrl": piscsi_ind_gpio,
},
]
# List of all of the SCSI signals that is also a dictionary to their human readable name
scsi_signals = {
scsi_d0_gpio: "D0",
scsi_d1_gpio: "D1",
scsi_d2_gpio: "D2",
scsi_d3_gpio: "D3",
scsi_d4_gpio: "D4",
scsi_d5_gpio: "D5",
scsi_d6_gpio: "D6",
scsi_d7_gpio: "D7",
scsi_dp_gpio: "DP",
scsi_atn_gpio: "ATN",
scsi_rst_gpio: "RST",
scsi_ack_gpio: "ACK",
scsi_req_gpio: "REQ",
scsi_msg_gpio: "MSG",
scsi_cd_gpio: "CD",
scsi_io_gpio: "IO",
scsi_bsy_gpio: "BSY",
scsi_sel_gpio: "SEL",
}
# Debug function that just dumps the status of all of the scsi signals to the console
def print_all():
for cur_gpio in gpio_map:
print(
cur_gpio["name"] + "=" + str(gpio.input(cur_gpio["gpio_num"])) + " ",
end="",
flush=True,
)
print("")
# Set transceivers IC1 and IC2 to OUTPUT
def set_dtd_out():
gpio.output(piscsi_dtd_gpio, gpio.LOW)
# Set transceivers IC1 and IC2 to INPUT
def set_dtd_in():
gpio.output(piscsi_dtd_gpio, gpio.HIGH)
# Set transceiver IC4 to OUTPUT
def set_ind_out():
gpio.output(piscsi_ind_gpio, gpio.HIGH)
# Set transceiver IC4 to INPUT
def set_ind_in():
gpio.output(piscsi_ind_gpio, gpio.LOW)
# Set transceiver IC3 to OUTPUT
def set_tad_out():
gpio.output(piscsi_tad_gpio, gpio.HIGH)
# Set transceiver IC3 to INPUT
def set_tad_in():
gpio.output(piscsi_tad_gpio, gpio.LOW)
# Set the specified transciever to an OUTPUT. All of the other transceivers
# will be set to inputs. If a non-existent direction gpio is specified, this
# will set all of the transceivers to inputs.
def set_output_channel(out_gpio):
if out_gpio == piscsi_tad_gpio:
set_tad_out()
else:
set_tad_in()
if out_gpio == piscsi_dtd_gpio:
set_dtd_out()
else:
set_dtd_in()
if out_gpio == piscsi_ind_gpio:
set_ind_out()
else:
set_ind_in()
# Main test procedure. This will execute for each of the SCSI pins to make sure its connected
# properly.
def test_gpio_pin(gpio_rec):
global err_count
set_output_channel(gpio_rec["dir_ctrl"])
############################################
# set the test gpio low
gpio.output(gpio_rec["gpio_num"], gpio.LOW)
time.sleep(pin_settle_delay)
# loop through all of the gpios
for cur_gpio in scsi_signals:
# all of the gpios should be high except for the test gpio and the connected gpio
cur_val = gpio.input(cur_gpio)
if cur_gpio == gpio_rec["gpio_num"]:
if cur_val != gpio.LOW:
print(
"Error: Test commanded GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " to be low, but it did not respond"
)
err_count = err_count + 1
elif cur_gpio == gpio_rec["attached_to"]:
if cur_val != gpio.LOW:
print(
"Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " should drive "
+ scsi_signals[gpio_rec["attached_to"]]
+ " low, but did not"
)
err_count = err_count + 1
else:
if cur_val != gpio.HIGH:
print(
"Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " incorrectly pulled "
+ scsi_signals[cur_gpio]
+ " LOW, when it shouldn't have"
)
err_count = err_count + 1
############################################
# set the transceivers to input
set_output_channel(piscsi_none)
time.sleep(pin_settle_delay)
# loop through all of the gpios
for cur_gpio in scsi_signals:
# all of the gpios should be high except for the test gpio
cur_val = gpio.input(cur_gpio)
if cur_gpio == gpio_rec["gpio_num"]:
if cur_val != gpio.LOW:
print(
"Error: Test commanded GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " to be low, but it did not respond"
)
err_count = err_count + 1
else:
if cur_val != gpio.HIGH:
print(
"Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " incorrectly pulled "
+ scsi_signals[cur_gpio]
+ " LOW, when it shouldn't have"
)
err_count = err_count + 1
# Set the transceiver back to output
set_output_channel(gpio_rec["dir_ctrl"])
#############################################
# set the test gpio high
gpio.output(gpio_rec["gpio_num"], gpio.HIGH)
time.sleep(pin_settle_delay)
# loop through all of the gpios
for cur_gpio in scsi_signals:
# all of the gpios should be high
cur_val = gpio.input(cur_gpio)
if cur_gpio == gpio_rec["gpio_num"]:
if cur_val != gpio.HIGH:
print(
"Error: Test commanded GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " to be high, but it did not respond"
)
err_count = err_count + 1
else:
if cur_val != gpio.HIGH:
print(
"Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " incorrectly pulled "
+ scsi_signals[cur_gpio]
+ " LOW, when it shouldn't have"
)
err_count = err_count + 1
# Initialize the GPIO library, set all of the gpios associated with SCSI signals to outputs and set
# all of the direction control gpios to outputs
def setup():
gpio.setmode(gpio.BOARD)
gpio.setwarnings(False)
for cur_gpio in gpio_map:
gpio.setup(cur_gpio["gpio_num"], gpio.OUT, initial=gpio.HIGH)
# Setup direction control
gpio.setup(piscsi_ind_gpio, gpio.OUT)
gpio.setup(piscsi_tad_gpio, gpio.OUT)
gpio.setup(piscsi_dtd_gpio, gpio.OUT)
# Main functions for running the actual test.
if __name__ == "__main__":
# setup the GPIOs
setup()
# Test each SCSI signal in the gpio_map
for cur_gpio in gpio_map:
test_gpio_pin(cur_gpio)
# Print the test results
if err_count == 0:
print("-------- Test PASSED --------")
else:
print("!!!!!!!! Test FAILED !!!!!!!!")
print("Total errors: " + str(err_count))
gpio.cleanup()

View File

@ -14,6 +14,8 @@ class ReturnCodeMapper:
_("File deleted: %(file_path)s"), _("File deleted: %(file_path)s"),
ReturnCodes.DELETEFILE_FILE_NOT_FOUND: ReturnCodes.DELETEFILE_FILE_NOT_FOUND:
_("File to delete not found: %(file_path)s"), _("File to delete not found: %(file_path)s"),
ReturnCodes.DELETEFILE_UNABLE_TO_DELETE:
_("Could not delete file: %(file_path)s"),
ReturnCodes.RENAMEFILE_SUCCESS: ReturnCodes.RENAMEFILE_SUCCESS:
_("File moved to: %(target_path)s"), _("File moved to: %(target_path)s"),
ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE: ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE:
@ -26,6 +28,8 @@ class ReturnCodeMapper:
_("File created: %(target_path)s"), _("File created: %(target_path)s"),
ReturnCodes.WRITEFILE_COULD_NOT_WRITE: ReturnCodes.WRITEFILE_COULD_NOT_WRITE:
_("Could not create file: %(target_path)s"), _("Could not create file: %(target_path)s"),
ReturnCodes.WRITEFILE_COULD_NOT_OVERWRITE:
_("A file with name %(target_path)s already exists"),
ReturnCodes.READCONFIG_SUCCESS: ReturnCodes.READCONFIG_SUCCESS:
_("Loaded configurations from: %(file_name)s"), _("Loaded configurations from: %(file_name)s"),
ReturnCodes.READCONFIG_COULD_NOT_READ: ReturnCodes.READCONFIG_COULD_NOT_READ:

View File

@ -104,6 +104,15 @@ ul.inline_list {
list-style: none; list-style: none;
} }
summary.dirname {
text-decoration: underline;
font-family: monospace;
}
summary.filename {
text-decoration: underline;
}
.dropzone, .dropzone * { .dropzone, .dropzone * {
box-sizing: border-box; box-sizing: border-box;
} }

View File

@ -53,6 +53,15 @@ input[type="radio"] {
margin: 0 0.1rem 0 0.75rem; margin: 0 0.1rem 0 0.75rem;
} }
div.notice {
background: var(--danger);
border-radius: var(--border-radius);
padding: 0.5rem;
font-size: 0.75rem;
display: inline-block;
color: #fff;
}
/* /*
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
Tables Tables
@ -708,6 +717,16 @@ section#files p {
margin-top: 1rem; margin-top: 1rem;
} }
section#files details.subdir summary.dirname {
text-decoration: underline;
font-family: monospace;
margin: 0.5rem 0;
}
section#files details.contents summary.filename {
text-decoration: underline;
}
@media (max-width: 900px) { @media (max-width: 900px) {
section#files table#images tr th:nth-child(2), section#files table#images tr th:nth-child(2),
section#files table#images tr td:nth-child(2) { section#files table#images tr td:nth-child(2) {

View File

@ -23,20 +23,6 @@
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="{{ url_for('static', filename=current_theme_stylesheet) }}"> <link rel="stylesheet" href="{{ url_for('static', filename=current_theme_stylesheet) }}">
<script type="application/javascript">
var processNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class=\"info\"><div>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div></div>";
window.scrollTo(0,0);
}
var shutdownNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class=\"warning\"><div>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div></div>";
window.scrollTo(0,0);
}
</script>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
</head> </head>
<body class="{{ body_classes|join(' ') }}"> <body class="{{ body_classes|join(' ') }}">
@ -131,7 +117,7 @@
{% endif %} {% endif %}
</div> </div>
<div> <div>
{{ _("PiSCSI version:") }} <b>{{ env["version"] }} <a href="https://github.com/PiSCSI/piscsi/commit/{{ env["running_env"]["git"] }}" target="_blank">{{ env["running_env"]["git"][:7] }}</a></b> {{ _("PiSCSI software version:") }} <b>{{ env["version"] }}</b>
</div> </div>
<div> <div>
{{ _("Hardware and OS:") }} {{ env["running_env"]["env"] }} {{ _("Hardware and OS:") }} {{ env["running_env"]["env"] }}

View File

@ -1,6 +1,18 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<script type="application/javascript">
var processNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class=\"info\"><div>" + Notification + "{{ _(" This process may take a while, and will continue in the background if you navigate away from this page.") }}</div></div>";
window.scrollTo(0,0);
}
var shutdownNotify = function(Notification) {
document.getElementById("flash").innerHTML = "<div class=\"warning\"><div>" + Notification + "{{ _(" The Web Interface will become unresponsive momentarily. Reload this page after the Pi has started up again.") }}</div></div>";
window.scrollTo(0,0);
}
</script>
<section id="current-config"> <section id="current-config">
<details> <details>
<summary class="heading"> <summary class="heading">
@ -197,6 +209,19 @@
</ul> </ul>
</details> </details>
{% if not files|length: %}
<div class="notice">
{{ _("The images directory is currently empty.") }}
</div>
{% else %}
<div>
{% for subdir, group in formatted_image_files.items() %}
<details class="subdir"{% if subdir == "images/" %} open{% endif %}>
<summary class="dirname">
{{ subdir }}
</summary>
<table id="images" border="black" cellpadding="3" summary="List of files in the image directory"> <table id="images" border="black" cellpadding="3" summary="List of files in the image directory">
<tbody> <tbody>
<tr> <tr>
@ -204,19 +229,12 @@
<th scope="col">{{ _("Size") }}</th> <th scope="col">{{ _("Size") }}</th>
<th scope="col">{{ _("Actions") }}</th> <th scope="col">{{ _("Actions") }}</th>
</tr> </tr>
{% if not files|length: %} {% for file in group|sort(attribute='name') %}
<tr class="directory-empty">
<td colspan="3">
{{ _("The images directory is currently empty.") }}
</td>
</tr>
{% endif %}
{% for file in files|sort(attribute='name') %}
<tr> <tr>
{% if file["prop"] %} {% if file["prop"] %}
<td> <td>
<details> <details class="contents">
<summary> <summary class="filename">
{{ file["name"] }} {{ file["name"] }}
</summary> </summary>
<ul class="inline_list"> <ul class="inline_list">
@ -232,8 +250,8 @@
</td> </td>
{% elif file["archive_contents"] %} {% elif file["archive_contents"] %}
<td> <td>
<details> <details class="contents">
<summary> <summary class="filename">
{{ file["name"] }} {{ file["name"] }}
</summary> </summary>
<ul class="inline_list"> <ul class="inline_list">
@ -241,8 +259,8 @@
{% if not member["is_properties_file"] %} {% if not member["is_properties_file"] %}
<li> <li>
{% if member["related_properties_file"] %} {% if member["related_properties_file"] %}
<details> <details id="contents">
<summary> <summary class="filename">
<label>{{ member["path"] }}</label> <label>{{ member["path"] }}</label>
<form action="/files/extract_image" method="post" class="file-extract"> <form action="/files/extract_image" method="post" class="file-extract">
<input name="archive_file" type="hidden" value="{{ file['name'] }}"> <input name="archive_file" type="hidden" value="{{ file['name'] }}">
@ -323,14 +341,14 @@
<input type="submit" value="{{ _("Attach") }}" title="{{ _("Attach") }}"> <input type="submit" value="{{ _("Attach") }}" title="{{ _("Attach") }}">
{% endif %} {% endif %}
</form> </form>
<form action="/files/rename" method="post" class="file-rename" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ loop.index }}').value = new_file_name;"> <form action="/files/rename" method="post" class="file-rename" onsubmit="var new_file_name = prompt('{{ _("Enter new file name for: %(file_name)s", file_name=file["name"]) }}', '{{ file['name'] }}'); if (new_file_name === null) event.preventDefault(); document.getElementById('new_file_name_{{ subdir }}_{{ loop.index }}').value = new_file_name;">
<input name="file_name" type="hidden" value="{{ file['name'] }}"> <input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="new_file_name" id="new_file_name_{{ loop.index }}" type="hidden" value=""> <input name="new_file_name" id="new_file_name_{{ subdir }}_{{ loop.index }}" type="hidden" value="">
<input type="submit" value="{{ _("Rename") }}" title="{{ _("Rename") }}"> <input type="submit" value="{{ _("Rename") }}" title="{{ _("Rename") }}">
</form> </form>
<form action="/files/copy" method="post" class="file-copy" onsubmit="var copy_file_name = prompt('{{ _("Save copy of %(file_name)s as:", file_name=file["name"]) }}', '{{ file['name'] }}'); if (copy_file_name === null) event.preventDefault(); document.getElementById('copy_file_name_{{ loop.index }}').value = copy_file_name;"> <form action="/files/copy" method="post" class="file-copy" onsubmit="var copy_file_name = prompt('{{ _("Save copy of %(file_name)s as:", file_name=file["name"]) }}', '{{ file['name'] }}'); if (copy_file_name === null) event.preventDefault(); document.getElementById('copy_file_name_{{ subdir }}_{{ loop.index }}').value = copy_file_name;">
<input name="file_name" type="hidden" value="{{ file['name'] }}"> <input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="copy_file_name" id="copy_file_name_{{ loop.index }}" type="hidden" value=""> <input name="copy_file_name" id="copy_file_name_{{ subdir }}_{{ loop.index }}" type="hidden" value="">
<input type="submit" value="{{ _("Copy") }}" title="{{ _("Copy") }}"> <input type="submit" value="{{ _("Copy") }}" title="{{ _("Copy") }}">
</form> </form>
<form action="/files/delete" method="post" class="file-delete" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')"> <form action="/files/delete" method="post" class="file-delete" onsubmit="return confirm('{{ _("Delete file: %(file_name)s?", file_name=file["name"]) }}')">
@ -349,6 +367,10 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</details>
{% endfor %}
</div>
{% endif %}
<p><small>{{ _("%(disk_space)s MiB disk space remaining on the system", disk_space=env["free_disk_space"]) }}</small></p> <p><small>{{ _("%(disk_space)s MiB disk space remaining on the system", disk_space=env["free_disk_space"]) }}</small></p>
</section> </section>
@ -361,8 +383,11 @@
</summary> </summary>
<ul> <ul>
<li>{{ _("Disk Images") }} = {{ env["image_dir"] }}</li> <li>{{ _("Disk Images") }} = {{ env["image_dir"] }}</li>
{% if file_server_dir_exists %}
<li>{{ _("Shared Files") }} = {{ FILE_SERVER_DIR }}</li> <li>{{ _("Shared Files") }} = {{ FILE_SERVER_DIR }}</li>
<li>{{ _("To access shared files remotely, you may have to install one of the file servers first.") }}</li> {% else %}
<li>{{ _("Install a file server and create the shared files directory in order to share files between the Pi and your vintage computers.") }}</li>
{% endif %}
</ul> </ul>
</details> </details>
@ -371,8 +396,22 @@
<input name="url" id="download_url" required="" type="url"> <input name="url" id="download_url" required="" type="url">
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked"> <input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="disk_images">{{ _("Disk Images") }}</label> <label for="disk_images">{{ _("Disk Images") }}</label>
<select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %}
<option value="{{dir}}">{{dir}}</option>
{% endfor %}
<option value="/" selected>/</option>
</select>
{% if file_server_dir_exists %}
<input type="radio" name="destination" id="shared_files" value="shared_files"> <input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_files">{{ _("Shared Files") }}</label> <label for="shared_files">{{ _("Shared Files") }}</label>
<select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %}
<option value="{{dir}}">{{dir}}</option>
{% endfor %}
<option value="/" selected>/</option>
</select>
{% endif %}
<input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File...") }}')"> <input type="submit" value="{{ _("Download") }}" onclick="processNotify('{{ _("Downloading File...") }}')">
</form> </form>
</section> </section>
@ -380,6 +419,14 @@
<section id="upload"> <section id="upload">
<a href="/upload" target="_blank"><p>{{ _("Upload Files (new tab)") }}</p></a> <a href="/upload" target="_blank"><p>{{ _("Upload Files (new tab)") }}</p></a>
</section> </section>
<noscript>
<style type="text/css">
section#upload { display: none; }
</style>
<div class="notice">
{{ _("The file uploading functionality requires JavaScript.") }}
</div>
</noscript>
<hr/> <hr/>

View File

@ -6,7 +6,9 @@
<li>{{ _("The largest file size accepted in this form is %(max_file_size)s MiB. Use other file transfer means for larger files.", max_file_size=max_file_size) }}</li> <li>{{ _("The largest file size accepted in this form is %(max_file_size)s MiB. Use other file transfer means for larger files.", max_file_size=max_file_size) }}</li>
<li>{{ _("You have to manually clean up partially uploaded files, as a result of cancelling the upload or closing this page.") }}</li> <li>{{ _("You have to manually clean up partially uploaded files, as a result of cancelling the upload or closing this page.") }}</li>
<li>{{ _("Disk Images") }} = {{ env["image_dir"] }}</li> <li>{{ _("Disk Images") }} = {{ env["image_dir"] }}</li>
{% if file_server_dir_exists %}
<li>{{ _("Shared Files") }} = {{ FILE_SERVER_DIR }}</li> <li>{{ _("Shared Files") }} = {{ FILE_SERVER_DIR }}</li>
{% endif %}
<li>{{ _("PiSCSI Config") }} = {{ CFG_DIR }}</li> <li>{{ _("PiSCSI Config") }} = {{ CFG_DIR }}</li>
</ul> </ul>
@ -14,12 +16,28 @@
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper"> <form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper">
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked"> <input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="disk_images">{{ _("Disk Images") }}</label> <label for="disk_images">{{ _("Disk Images") }}</label>
<select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %}
<option value="{{dir}}">{{dir}}</option>
{% endfor %}
<option value="/" selected>/</option>
</select>
{% if file_server_dir_exists %}
<input type="radio" name="destination" id="shared_files" value="shared_files"> <input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_files">{{ _("Shared Files") }}</label> <label for="shared_files">{{ _("Shared Files") }}</label>
<select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %}
<option value="{{dir}}">{{dir}}</option>
{% endfor %}
<option value="/" selected>/</option>
</select>
{% endif %}
<input type="radio" name="destination" id="piscsi_config" value="piscsi_config"> <input type="radio" name="destination" id="piscsi_config" value="piscsi_config">
<label for="piscsi_config">{{ _("PiSCSI Config") }}</label> <label for="piscsi_config">{{ _("PiSCSI Config") }}</label>
</form> </form>
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
<script type="application/javascript"> <script type="application/javascript">
Dropzone.options.dropper = { Dropzone.options.dropper = {
paramName: 'file', paramName: 'file',
@ -54,4 +72,10 @@
} }
</script> </script>
<noscript>
<div class="noscriptmsg">
{{ _("The file uploading functionality requires JavaScript.") }}
</div>
</noscript>
{% endblock content %} {% endblock content %}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,6 @@ from flask import (
send_from_directory, send_from_directory,
make_response, make_response,
session, session,
abort,
jsonify, jsonify,
) )
@ -44,11 +43,13 @@ from return_code_mapper import ReturnCodeMapper
from socket_cmds_flask import SocketCmdsFlask from socket_cmds_flask import SocketCmdsFlask
from web_utils import ( from web_utils import (
working_dirs_exist,
sort_and_format_devices, sort_and_format_devices,
get_valid_scsi_ids, get_valid_scsi_ids,
map_device_types_and_names, map_device_types_and_names,
get_device_name, get_device_name,
map_image_file_descriptions, map_image_file_descriptions,
format_image_list,
format_drive_properties, format_drive_properties,
get_properties_by_drive_name, get_properties_by_drive_name,
auth_active, auth_active,
@ -208,27 +209,15 @@ def index():
""" """
Sets up data structures for and renders the index page Sets up data structures for and renders the index page
""" """
if not piscsi_cmd.is_token_auth()["status"] and not APP.config["PISCSI_TOKEN"]:
abort(
403,
_(
"PiSCSI is password protected. "
"Start the Web Interface with the --password parameter."
),
)
server_info = piscsi_cmd.get_server_info() server_info = piscsi_cmd.get_server_info()
working_dirs_exist((server_info["image_dir"], CFG_DIR))
devices = piscsi_cmd.list_devices() devices = piscsi_cmd.list_devices()
device_types = map_device_types_and_names(piscsi_cmd.get_device_types()["device_types"]) device_types = map_device_types_and_names(piscsi_cmd.get_device_types()["device_types"])
image_files = file_cmd.list_images() image_files = file_cmd.list_images()
config_files = file_cmd.list_config_files() config_files = file_cmd.list_config_files()
ip_addr, host = sys_cmd.get_ip_and_host() ip_addr, host = sys_cmd.get_ip_and_host()
formatted_image_files = format_image_list(image_files["files"], device_types)
extended_image_files = []
for image in image_files["files"]:
if image["detected_type"] != "UNDEFINED":
image["detected_type_name"] = device_types[image["detected_type"]]["name"]
extended_image_files.append(image)
attached_images = [] attached_images = []
units = 0 units = 0
@ -266,7 +255,8 @@ def index():
bridge_configured=sys_cmd.is_bridge_setup(), bridge_configured=sys_cmd.is_bridge_setup(),
devices=formatted_devices, devices=formatted_devices,
attached_images=attached_images, attached_images=attached_images,
files=extended_image_files, formatted_image_files=formatted_image_files,
files=image_files["files"],
config_files=config_files, config_files=config_files,
device_types=device_types, device_types=device_types,
scan_depth=server_info["scan_depth"], scan_depth=server_info["scan_depth"],
@ -278,6 +268,9 @@ def index():
image_suffixes_to_create=image_suffixes_to_create, image_suffixes_to_create=image_suffixes_to_create,
valid_image_suffixes=valid_image_suffixes, valid_image_suffixes=valid_image_suffixes,
drive_properties=format_drive_properties(APP.config["PISCSI_DRIVE_PROPERTIES"]), drive_properties=format_drive_properties(APP.config["PISCSI_DRIVE_PROPERTIES"]),
images_subdirs=file_cmd.list_subdirs(server_info["image_dir"]),
shared_subdirs=file_cmd.list_subdirs(FILE_SERVER_DIR),
file_server_dir_exists=Path(FILE_SERVER_DIR).exists(),
RESERVATIONS=RESERVATIONS, RESERVATIONS=RESERVATIONS,
CFG_DIR=CFG_DIR, CFG_DIR=CFG_DIR,
FILE_SERVER_DIR=FILE_SERVER_DIR, FILE_SERVER_DIR=FILE_SERVER_DIR,
@ -303,6 +296,8 @@ def drive_list():
""" """
Sets up the data structures and kicks off the rendering of the drive list page Sets up the data structures and kicks off the rendering of the drive list page
""" """
server_info = piscsi_cmd.get_server_info()
working_dirs_exist((server_info["image_dir"], CFG_DIR))
return response( return response(
template="drives.html", template="drives.html",
@ -317,10 +312,15 @@ def upload_page():
""" """
Sets up the data structures and kicks off the rendering of the file uploading page Sets up the data structures and kicks off the rendering of the file uploading page
""" """
server_info = piscsi_cmd.get_server_info()
working_dirs_exist((server_info["image_dir"], CFG_DIR))
return response( return response(
template="upload.html", template="upload.html",
page_title=_("PiSCSI File Upload"), page_title=_("PiSCSI File Upload"),
images_subdirs=file_cmd.list_subdirs(server_info["image_dir"]),
shared_subdirs=file_cmd.list_subdirs(FILE_SERVER_DIR),
file_server_dir_exists=Path(FILE_SERVER_DIR).exists(),
max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024), max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024),
CFG_DIR=CFG_DIR, CFG_DIR=CFG_DIR,
FILE_SERVER_DIR=FILE_SERVER_DIR, FILE_SERVER_DIR=FILE_SERVER_DIR,
@ -514,6 +514,7 @@ def show_diskinfo():
if not safe_path["status"]: if not safe_path["status"]:
return response(error=True, message=safe_path["msg"]) return response(error=True, message=safe_path["msg"])
server_info = piscsi_cmd.get_server_info() server_info = piscsi_cmd.get_server_info()
working_dirs_exist((server_info["image_dir"], CFG_DIR))
returncode, diskinfo = sys_cmd.get_diskinfo(Path(server_info["image_dir"]) / file_name) returncode, diskinfo = sys_cmd.get_diskinfo(Path(server_info["image_dir"]) / file_name)
if returncode == 0: if returncode == 0:
return response( return response(
@ -959,11 +960,22 @@ def download_file():
""" """
destination = request.form.get("destination") destination = request.form.get("destination")
url = request.form.get("url") url = request.form.get("url")
if destination == "shared_files": images_subdir = request.form.get("images_subdir")
destination_dir = FILE_SERVER_DIR shared_subdir = request.form.get("shared_subdir")
else: if destination == "disk_images":
safe_path = is_safe_path(Path("." + images_subdir))
if not safe_path["status"]:
return make_response(safe_path["msg"], 403)
server_info = piscsi_cmd.get_server_info() server_info = piscsi_cmd.get_server_info()
destination_dir = server_info["image_dir"] destination_dir = server_info["image_dir"] + images_subdir
elif destination == "shared_files":
safe_path = is_safe_path(Path("." + shared_subdir))
if not safe_path["status"]:
return make_response(safe_path["msg"], 403)
destination_dir = FILE_SERVER_DIR + shared_subdir
else:
return response(error=True, message=_("Unknown destination"))
process = file_cmd.download_to_dir(url, destination_dir, Path(url).name) process = file_cmd.download_to_dir(url, destination_dir, Path(url).name)
process = ReturnCodeMapper.add_msg(process) process = ReturnCodeMapper.add_msg(process)
if process["status"]: if process["status"]:
@ -990,15 +1002,23 @@ def upload_file():
return make_response(auth["msg"], 403) return make_response(auth["msg"], 403)
destination = request.form.get("destination") destination = request.form.get("destination")
images_subdir = request.form.get("images_subdir")
shared_subdir = request.form.get("shared_subdir")
if destination == "disk_images": if destination == "disk_images":
safe_path = is_safe_path(Path("." + images_subdir))
if not safe_path["status"]:
return make_response(safe_path["msg"], 403)
server_info = piscsi_cmd.get_server_info() server_info = piscsi_cmd.get_server_info()
destination_dir = server_info["image_dir"] destination_dir = server_info["image_dir"] + images_subdir
elif destination == "shared_files": elif destination == "shared_files":
destination_dir = FILE_SERVER_DIR safe_path = is_safe_path(Path("." + shared_subdir))
if not safe_path["status"]:
return make_response(safe_path["msg"], 403)
destination_dir = FILE_SERVER_DIR + shared_subdir
elif destination == "piscsi_config": elif destination == "piscsi_config":
destination_dir = CFG_DIR destination_dir = CFG_DIR
else: else:
return make_response("Invalid destination", 403) return make_response(_("Unknown destination"), 403)
return upload_with_dropzonejs(destination_dir) return upload_with_dropzonejs(destination_dir)
@ -1453,6 +1473,12 @@ if __name__ == "__main__":
file_cmd = FileCmds(sock_cmd=sock_cmd, piscsi=piscsi_cmd, token=APP.config["PISCSI_TOKEN"]) file_cmd = FileCmds(sock_cmd=sock_cmd, piscsi=piscsi_cmd, token=APP.config["PISCSI_TOKEN"])
sys_cmd = SysCmds() sys_cmd = SysCmds()
if not piscsi_cmd.is_token_auth()["status"] and not APP.config["PISCSI_TOKEN"]:
raise Exception(
"PiSCSI is password protected. "
"Start the Web Interface with the --password parameter."
)
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file(): if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
file_cmd.read_config(DEFAULT_CONFIG) file_cmd.read_config(DEFAULT_CONFIG)
if Path(f"{DRIVE_PROPERTIES_FILE}").is_file(): if Path(f"{DRIVE_PROPERTIES_FILE}").is_file():

View File

@ -7,14 +7,28 @@ from grp import getgrall
from os import path from os import path
from pathlib import Path from pathlib import Path
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
from re import findall
from flask import request, make_response from flask import request, make_response, abort
from flask_babel import _ from flask_babel import _
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from piscsi.sys_cmds import SysCmds from piscsi.sys_cmds import SysCmds
def working_dirs_exist(working_dirs):
"""
Method for validating that working dirs exist.
Takes (tuple) of (str) working_dirs with paths to required dirs.
"""
for dir_path in working_dirs:
if not Path(dir_path).exists():
abort(
503,
_(f"Please create directory: {dir_path}"),
)
def get_valid_scsi_ids(devices, reserved_ids): def get_valid_scsi_ids(devices, reserved_ids):
""" """
Takes a list of (dict)s devices, and list of (int)s reserved_ids. Takes a list of (dict)s devices, and list of (int)s reserved_ids.
@ -146,6 +160,33 @@ def get_image_description(file_suffix):
return file_suffix return file_suffix
def format_image_list(image_files, device_types=None):
"""
Takes a (list) of (dict) image_files and optional (list) device_types
Returns a formatted (dict) with groups of image_files per subdir key
"""
root_image_files = []
subdir_image_files = {}
for image in image_files:
if (image["detected_type"] != "UNDEFINED") and device_types:
image["detected_type_name"] = device_types[image["detected_type"]]["name"]
subdir_path = findall("^.*/", image["name"])
if subdir_path:
subdir = subdir_path[0]
if f"images/{subdir}" in subdir_image_files.keys():
subdir_image_files[f"images/{subdir}"].append(image)
else:
subdir_image_files[f"images/{subdir}"] = [image]
else:
root_image_files.append(image)
formatted_image_files = dict(sorted(subdir_image_files.items()))
if root_image_files:
formatted_image_files["images/"] = root_image_files
return formatted_image_files
def format_drive_properties(drive_properties): def format_drive_properties(drive_properties):
""" """
Takes a (dict) with structured drive properties data Takes a (dict) with structured drive properties data
@ -256,10 +297,10 @@ def is_safe_path(file_name):
Returns True if the path is safe Returns True if the path is safe
Returns False if the path is either absolute, or tries to traverse the file system Returns False if the path is either absolute, or tries to traverse the file system
""" """
if file_name.is_absolute() or ".." in str(file_name): if file_name.is_absolute() or ".." in str(file_name) or str(file_name)[0] == "~":
return { return {
"status": False, "status": False,
"msg": _("%(file_name)s is not a valid path", file_name=file_name), "msg": _("No permission to use path '%(file_name)s'", file_name=file_name),
} }
return {"status": True, "msg": ""} return {"status": True, "msg": ""}

View File

@ -207,7 +207,8 @@ def test_extract_file(
http_client.post( http_client.post(
"/files/download_url", "/files/download_url",
data={ data={
"destination": "images", "destination": "disk_images",
"images_subdir": "/",
"url": url, "url": url,
}, },
) )
@ -254,6 +255,7 @@ def test_upload_file(http_client, delete_file):
form_data = { form_data = {
"destination": "disk_images", "destination": "disk_images",
"images_subdir": "/",
"dzuuid": str(uuid.uuid4()), "dzuuid": str(uuid.uuid4()),
"dzchunkindex": chunk_number, "dzchunkindex": chunk_number,
"dzchunksize": chunk_size, "dzchunksize": chunk_size,
@ -333,6 +335,7 @@ def test_download_properties(http_client, list_files, delete_file):
def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_file): def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_file):
file_name = str(uuid.uuid4()) file_name = str(uuid.uuid4())
http_path = f"/images/{file_name}" http_path = f"/images/{file_name}"
subdir = "/"
url = httpserver.url_for(http_path) url = httpserver.url_for(http_path)
with open("tests/assets/test_image.hds", mode="rb") as file: with open("tests/assets/test_image.hds", mode="rb") as file:
@ -346,7 +349,8 @@ def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_fi
response = http_client.post( response = http_client.post(
"/files/download_url", "/files/download_url",
data={ data={
"destination": "images", "destination": "disk_images",
"images_subdir": subdir,
"url": url, "url": url,
}, },
) )
@ -357,7 +361,8 @@ def test_download_url_to_dir(env, httpserver, http_client, list_files, delete_fi
assert response_data["status"] == STATUS_SUCCESS assert response_data["status"] == STATUS_SUCCESS
assert file_name in list_files() assert file_name in list_files()
assert ( assert (
response_data["messages"][0]["message"] == f"{file_name} downloaded to {env['images_dir']}" response_data["messages"][0]["message"]
== f"{file_name} downloaded to {env['images_dir']}{subdir}"
) )
# Cleanup # Cleanup